from __future__ import annotations from .shared import * from ...services.sstc.entity_lookup import _ensure_project from ...services.sstc.feishu_routing import _backfill_feishu_sender_names from ...services.sstc.signal_core import _as_dict, _extract_signal_attachments, _normalize_planner_status, _normalize_signal_status_code, _parse_project_id, _signal_ai_analysis, _signal_display_type, _signal_project_scope, _signal_source_context, _signal_status_label_from_code from ...services.sstc.signal_filters import _build_signal_created_at_range, _extract_charter_related_signal_ids, _match_signal_filters, _normalize_signal_status_filter, _signal_proposer_name, _signal_title_text router = APIRouter(tags=["sstc"]) @router.get("/projects/{project_id}/signals") async def list_signals( project_id: str, status: Optional[str] = Query(default=None, description="pending/linked/closed/archived"), keyword: Optional[str] = Query(default=None, description="按信号标题/内容/ID/提报人/关联宪章搜索"), source: Optional[str] = Query(default=None, description="来源过滤"), priority: Optional[str] = Query(default=None, description="优先级过滤"), created_from: Optional[str] = Query(default=None, description="创建时间起始(北京时间 YYYY-MM-DD)"), created_to: Optional[str] = Query(default=None, description="创建时间结束(北京时间 YYYY-MM-DD)"), limit: int = Query(default=10, ge=1, le=100), offset: int = Query(default=0, ge=0), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): pid = _parse_project_id(project_id) tenant_id = int(current_user.tenant_id) svc = SSTCService(db, tenant_id=tenant_id) project = await _ensure_project(svc, pid) project_creator_name = "-" if project and getattr(project, "owner_id", None): owner = ( await db.execute( select(User.name).where( and_( User.tenant_id == tenant_id, User.id == int(project.owner_id), ) ).limit(1) ) ).scalars().first() if owner: project_creator_name = str(owner) signals = await svc.get_signals(pid) await _backfill_feishu_sender_names( db, tenant_id=tenant_id, signals=signals, ) charters = await svc.get_charters(pid) linked_signal_states: dict[int, str] = {} linked_signal_charter_name: dict[int, str] = {} for c in charters: signal_ids = _extract_charter_related_signal_ids(c) charter_status = (c.status or "").lower() state = ( _SIGNAL_STATUS_CODE_CLOSED if charter_status in {"aligned", "active", "online", "archived", "已对齐", "已上线", "已归档"} else _SIGNAL_STATUS_CODE_LINKED ) for sid in signal_ids: if linked_signal_states.get(sid) == _SIGNAL_STATUS_CODE_CLOSED: continue linked_signal_states[sid] = state linked_signal_charter_name[sid] = c.title normalized_status_filter = _normalize_signal_status_filter(status) created_from_utc, created_to_utc = _build_signal_created_at_range( created_from=created_from, created_to=created_to, ) rows = [] for s in signals: strategic_payload = _as_dict(s.strategic_analysis) ai_analysis = _signal_ai_analysis(s) source_context = _signal_source_context(s) project_scope = _signal_project_scope(s) proposer_name = _signal_proposer_name(s) title_text = _signal_title_text(s) normalized_status = str(s.status or "").strip() sid = int(s.id) status_code = _normalize_signal_status_code( linked_state=linked_signal_states.get(sid), signal_status=normalized_status, processed=bool(s.processed), ) row_status = _signal_status_label_from_code(status_code, normalized_status) locked_by_charter = sid in linked_signal_states or ("关联" in normalized_status) can_create_charter = not locked_by_charter charter_lock_reason = None if can_create_charter else _SIGNAL_LOCK_REASON row = { "id": str(s.id), "title": title_text[:80] or "未命名信号", "content": s.content, "priority": s.priority, "signal_type": _signal_display_type(s), "source": s.source, "project_creator_name": project_creator_name, "proposer_name": proposer_name, "proposer_open_id": source_context.get("sender_open_id"), "project_scope": project_scope, "scope_reason": ai_analysis.get("reason"), "target_project_hints": ai_analysis.get("target_project_hints", []), "status": row_status, "status_code": status_code, "can_create_charter": can_create_charter, "charter_lock_reason": charter_lock_reason, "charter_name": linked_signal_charter_name.get(sid), "created_at": s.created_at.isoformat() if s.created_at else None, "attachments": _extract_signal_attachments(s), "strategic_analysis": strategic_payload, "planner_status": _normalize_planner_status(getattr(s, "planner_status", None)), "planner_analyzed_at": ( s.planner_analyzed_at.isoformat() if getattr(s, "planner_analyzed_at", None) else None ), "planner_assignee_user_id": ( str(s.planner_assignee_user_id) if getattr(s, "planner_assignee_user_id", None) is not None else None ), } created_at_dt = s.created_at if created_at_dt is not None and created_at_dt.tzinfo is not None: created_at_dt = created_at_dt.astimezone(CN_TZ).replace(tzinfo=None) if not _match_signal_filters( row=row, status_filter=normalized_status_filter, keyword=keyword, source=source, priority=priority, created_from_utc=created_from_utc, created_to_utc=created_to_utc, created_at_dt=created_at_dt, ): continue rows.append(row) total = len(rows) paged_rows = rows[offset : offset + limit] return success_response( { "items": paged_rows, "total": total, "limit": limit, "offset": offset, "has_more": (offset + len(paged_rows)) < total, } )