Files
microfish/backend/app/__init__.py
Kunthawat Greethong 8b84378fe1 feat: SaaS foundation for CrowdSight
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.
2026-08-31 13:05:21 +07:00

123 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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