from fastapi import APIRouter, Cookie from app.db import auth_db from app.db.analytics import ( get_all_recommendations, get_cron_run_logs, get_cron_run_summary, get_observation_candidates, get_pipeline_run_detail, get_pipeline_runs, get_review_stats, get_screening_history, get_stats, ) from app.db.llm_insights import get_llm_insight_by_id, list_llm_insights from app.db.recommendation_queries import get_active_recommendations, get_active_recommendations_deduped, get_opportunity_detail from app.db.short_tf_signals import get_short_tf_signal_review from app.config.config_loader import get_signal_weights from app.web.shared import ( ObservationRequest, PushRulesRequest, WatchlistRequest, require_api_user_with_subscription, ) router = APIRouter() def _friendly_llm_item(item): content = item.get("content") or {} payload = item.get("input") or {} target_type = item.get("target_type") or "" status = item.get("status") or "" type_label = { "recommendation": "推荐解释", "sentiment": "舆情解读", "review": "复盘 memo", }.get(target_type, target_type or "未知任务") status_label = { "success": "成功", "failed": "失败", "skipped": "跳过", }.get(status, status or "未知") subject = payload.get("symbol") or payload.get("related_symbol") or payload.get("title") or payload.get("run_date") or item.get("target_id") summary = content.get("summary") or content.get("memo") or content.get("why_now_or_not") or content.get("raw") or item.get("error") or "" return { "id": item.get("id"), "type_label": type_label, "status_label": status_label, "status": status, "subject": subject, "summary": summary, "model": item.get("model") or "", "prompt_version": item.get("prompt_version") or "", "target_type": target_type, "target_id": item.get("target_id"), "updated_at": item.get("updated_at"), "error": item.get("error") or "", "content": content, "input": payload, } @router.get("/api/stats") async def api_stats(altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_stats() @router.get("/api/recommendations") async def api_recommendations( limit: int = 50, offset: int = 0, decision_only: bool = False, version: str = "", archive_filter: str = "", paged: bool = False, compact: bool = False, side: str = "", altcoin_session: str = Cookie(default=""), ): require_api_user_with_subscription(altcoin_session) return get_all_recommendations( limit, decision_only=decision_only, version=version, offset=offset, with_meta=(paged or compact), archive_filter=archive_filter, ) @router.get("/api/recommendations/active") async def api_recommendations_active( dedup: bool = True, actionable_only: bool = True, version: str = "", hours: float = 0, limit: int = 0, offset: int = 0, paged: bool = False, compact: bool = False, side: str = "", altcoin_session: str = Cookie(default=""), ): require_api_user_with_subscription(altcoin_session) if dedup: return get_active_recommendations_deduped( actionable_only=actionable_only, version=version, hours=hours, limit=limit, offset=offset, with_meta=(paged or compact), side=side, ) return get_active_recommendations(actionable_only=actionable_only) @router.get("/api/opportunity/detail") async def api_opportunity_detail(symbol: str = "", rec_id: int = 0, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) detail = get_opportunity_detail(symbol=symbol, rec_id=rec_id) if not detail: return {"error": "opportunity not found", "symbol": symbol, "rec_id": rec_id} return detail @router.get("/api/observations/active") async def api_observations_active( limit: int = 50, altcoin_session: str = Cookie(default=""), ): require_api_user_with_subscription(altcoin_session) return get_observation_candidates(limit=limit) @router.get("/api/personalization") async def api_personalization(altcoin_session: str = Cookie(default="")): user = require_api_user_with_subscription(altcoin_session) return { "watchlist": auth_db.get_watchlist_symbols(user["id"]), "observations": auth_db.get_saved_observations(user["id"]), "push_rules": auth_db.get_push_rules(user["id"]), } @router.post("/api/watchlist") async def api_add_watchlist(req: WatchlistRequest, altcoin_session: str = Cookie(default="")): user = require_api_user_with_subscription(altcoin_session) auth_db.add_watchlist_symbol(user["id"], req.symbol) return {"ok": True, "watchlist": auth_db.get_watchlist_symbols(user["id"])} @router.delete("/api/watchlist/{symbol}") async def api_remove_watchlist(symbol: str, altcoin_session: str = Cookie(default="")): user = require_api_user_with_subscription(altcoin_session) auth_db.remove_watchlist_symbol(user["id"], symbol) return {"ok": True, "watchlist": auth_db.get_watchlist_symbols(user["id"])} @router.post("/api/observations") async def api_save_observation(req: ObservationRequest, altcoin_session: str = Cookie(default="")): user = require_api_user_with_subscription(altcoin_session) auth_db.save_observation(user["id"], req.rec_id, req.note) return {"ok": True, "observations": auth_db.get_saved_observations(user["id"])} @router.delete("/api/observations/{rec_id}") async def api_remove_observation(rec_id: int, altcoin_session: str = Cookie(default="")): user = require_api_user_with_subscription(altcoin_session) auth_db.remove_observation(user["id"], rec_id) return {"ok": True, "observations": auth_db.get_saved_observations(user["id"])} @router.post("/api/push-rules") async def api_update_push_rules(req: PushRulesRequest, altcoin_session: str = Cookie(default="")): user = require_api_user_with_subscription(altcoin_session) rules = auth_db.update_push_rules(user["id"], req.dict()) return {"ok": True, "push_rules": rules} @router.get("/api/screening") async def api_screening(hours: int = 24, limit: int = 100, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_screening_history(hours, limit) @router.get("/api/screening/short-tf-review") async def api_short_tf_signal_review(hours: int = 168, limit: int = 200, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_short_tf_signal_review(hours=hours, limit=limit) @router.get("/api/review") async def api_review(altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_review_stats() @router.get("/api/weights") async def api_weights(altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_signal_weights() @router.get("/api/cron") async def api_cron(limit: int = 50, job_name: str = "", altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_cron_run_logs(limit=limit, job_name=job_name or None) @router.get("/api/cron/summary") async def api_cron_summary(hours: int = 24, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_cron_run_summary(hours=hours) @router.get("/api/pipeline/runs") async def api_pipeline_runs(limit: int = 30, hours: int = 24, offset: int = 0, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) return get_pipeline_runs(limit=limit, hours=hours, offset=offset) @router.get("/api/pipeline/runs/{run_id}") async def api_pipeline_run_detail(run_id: int, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) detail = get_pipeline_run_detail(run_id) if not detail: return {"error": "pipeline run not found", "run_id": run_id} return detail @router.get("/api/llm/insights") async def api_llm_insights( limit: int = 30, offset: int = 0, target_type: str = "", status: str = "", insight_type: str = "", altcoin_session: str = Cookie(default=""), ): require_api_user_with_subscription(altcoin_session) data = list_llm_insights( limit=limit, offset=offset, target_type=target_type or "", status=status or "", insight_type=insight_type or "", ) data["items"] = [_friendly_llm_item(item) for item in data.get("items", [])] return data @router.get("/api/llm/insights/{insight_id}") async def api_llm_insight_detail(insight_id: int, altcoin_session: str = Cookie(default="")): require_api_user_with_subscription(altcoin_session) item = get_llm_insight_by_id(insight_id) if not item: return {"error": "llm insight not found", "id": insight_id} return _friendly_llm_item(item)