分析LLM调用超时配置变化影响该请求

This commit is contained in:
Yuuuu 2026-03-11 16:41:29 +08:00
parent 2e358bc2a4
commit 7c9d274a6c
51 changed files with 436 additions and 161 deletions

View File

@ -1,6 +1,6 @@
{
"version": "v2",
"updated_at": "2026-03-11T00:00:00Z",
"updated_at": "2026-03-11T08:35:00Z",
"source_of_truth": "openclaw",
"description": "PMFlow 三引擎统一路由:所有主链路统一经 OpenClaw OpenAI-Compatible 网关。",
"rules": [
@ -10,7 +10,7 @@
"priority": 320,
"tier": "high",
"provider": "openai",
"model": "MiniMax-M2.5",
"model": "GLM-5",
"api_env": "OPENAI_API_KEY",
"function_exact": "classify_intent_complex"
},
@ -32,12 +32,12 @@
]
},
{
"name": "engine_high_reasoning_minimax",
"name": "engine_high_reasoning_glm5",
"enabled": true,
"priority": 220,
"tier": "high",
"provider": "openai",
"model": "MiniMax-M2.5",
"model": "GLM-5",
"api_env": "OPENAI_API_KEY",
"match_any": [
"adaptive_rag_answer",
@ -62,7 +62,7 @@
"priority": 200,
"tier": "high",
"provider": "openai",
"model": "MiniMax-M2.5",
"model": "GLM-5",
"api_env": "OPENAI_API_KEY",
"match_any": [
"analyze_food_calories",
@ -80,9 +80,9 @@
"enabled": true,
"priority": 50,
"tier": "safety",
"provider": "openai",
"model": "deepseek-chat",
"api_env": "OPENAI_API_KEY",
"provider": "glm",
"model": "GLM-4.7",
"api_env": "GLM_API_KEY",
"match_all": true
}
]

View File

@ -174,3 +174,4 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
- AI PMO 意图仲裁默认开启(`AI_PMO_INTENT_ARBITRATION_ENABLED=true`):规则与 LLM 写操作冲突时按 `AI_PMO_INTENT_CONFLICT_POLICY=pending_manual` 强制进入人工确认,不直接自动执行
- `project.create` 启用槽位完整性闸门:无法从指令提取有效项目名时标记 `slot_incomplete` 并进入人工确认
- 数据库与核心写入链路统一使用北京时间(`Asia/Shanghai`):PostgreSQL 会话时区、数据库默认时区与核心 `_utc_now*` 写入入口均已切换到北京时间
- 后端与脚本层已清零 `datetime.utcnow()/timezone.utc`:统一使用 `backend/app/utils/timezone.py`(`now_cn_aware/now_cn_naive/CN_TZ`)管理时间语义

View File

@ -67,4 +67,5 @@ def downgrade() -> None:
if not _is_postgresql():
return
_shift_all_timestamp_columns(hours=-8)
_set_database_timezone("UTC")
# 统一策略:即使降级历史数据,也保持数据库会话时区为北京时间。
_set_database_timezone("Asia/Shanghai")

View File

@ -33,9 +33,9 @@ from ..services.ai_orchestration_run_service import AIOrchestrationRunService
from ..services.openclaw_commander_service import OpenClawCommanderService
from ..services.signal_source_orchestration_service import SignalSourceOrchestrationService
from ..schemas.rag import RAGAskRequest
from ..utils.timezone import now_cn_naive
from ..utils.timezone import now_cn_aware, now_cn_naive
router = APIRouter(prefix="/ai-steward", tags=["AI Steward"])
@ -264,7 +264,7 @@ async def onboard_signal_source(
{
"source": "pmflow_manual_signal_source_onboard",
"instruction": request.instruction[:2000],
"created_at": datetime.now(timezone.utc).isoformat(),
"created_at": now_cn_aware().isoformat(),
},
ensure_ascii=False,
),
@ -1103,7 +1103,7 @@ async def get_ai_steward_stats(
'active_blockers': active_blockers,
'active_projects': active_projects,
'hours_saved': hours_saved,
'last_updated': datetime.now(timezone.utc).isoformat()
'last_updated': now_cn_aware().isoformat()
}
return stats

View File

@ -20,6 +20,7 @@ from ..schemas.budget import (
)
from ..services.budget_enforcement_service import BudgetEnforcementService
from ..utils.timezone import now_cn_naive
router = APIRouter(prefix="/budget", tags=["budget"])
@ -84,7 +85,7 @@ async def create_budget(
db: AsyncSession = Depends(get_db),
):
_ = current_user
now = datetime.utcnow()
now = now_cn_naive()
period_start, period_end = _period_window(now, request.period)
new_budget = Budget(
tenant_id=tenant_id,

View File

@ -15,6 +15,7 @@ from ..dependencies import get_current_user, get_tenant_id
from ..models import Notification, User
from ..utils.timezone import now_cn_naive
router = APIRouter(prefix="/notifications", tags=["notifications"])
@ -136,7 +137,7 @@ async def mark_notification_read(
if not row.is_read:
row.is_read = True
row.read_at = datetime.now(timezone.utc)
row.read_at = now_cn_naive()
await db.commit()
return success_response(data=_serialize_notification(row))
@ -160,7 +161,7 @@ async def mark_all_notifications_read(
)
).scalars().all()
now = datetime.now(timezone.utc)
now = now_cn_naive()
for row in rows:
row.is_read = True
row.read_at = now

View File

@ -17,6 +17,7 @@ from ..services.ai_pmo_kernel_service import AIPMOKernelService
from ..services import skill_service
from ..services.auth import has_any_role
from ..utils.timezone import now_cn_naive
router = APIRouter(tags=["skill-tags"])
@ -141,7 +142,7 @@ async def update_skill_tag(
tag.description = body.description
if body.sort_order is not None:
tag.sort_order = body.sort_order
tag.updated_at = datetime.utcnow()
tag.updated_at = now_cn_naive()
await db.commit()
await db.refresh(tag)
return _to_tag_payload(tag)
@ -285,7 +286,7 @@ async def update_user_skill(
row.quality_score = body.quality_score
if body.efficiency_score is not None:
row.efficiency_score = body.efficiency_score
row.updated_at = datetime.utcnow()
row.updated_at = now_cn_naive()
await db.commit()
await AIPMOKernelService.invalidate_world_model_cache_for_user(db=db, tenant_id=tenant_id, user_id=user_id)
return {"message": "Skill updated"}

View File

@ -79,6 +79,7 @@ from ..dependencies import get_current_user, require_project_member_or_manager
from ..utils.title import compact_title
from ..utils.timezone import CN_TZ, now_cn_aware, now_cn_naive
router = APIRouter(tags=["sstc"])
_FEISHU_PLACEHOLDER_RE = re.compile(r"^飞书用户\(([^)]+)\)$")
_SIGNAL_PREFIX_RE = re.compile(r"^\[(.*?)\]\s*:\s*")
@ -866,7 +867,7 @@ async def _build_ai_first_charter_content(
fallback_model = "同供应商低成本模型"
source_refs = [f"signal:{sid}" for sid in signal_ids]
now_iso = datetime.now(timezone.utc).isoformat()
now_iso = now_cn_aware().isoformat()
validations_m1 = _default_ai_validations("模块1:触发与目标")
validations_m2 = _default_ai_validations("模块2:范围与对齐")
validations_m3 = _default_ai_validations("模块3:模型与依赖")
@ -1397,7 +1398,7 @@ def _normalize_review_content(raw: dict[str, Any] | None) -> dict[str, Any]:
"what_went_well": [str(x).strip() for x in what_went_well if str(x).strip()],
"root_causes": [str(x).strip() for x in root_causes if str(x).strip()],
"improvements": normalized_improvements,
"last_updated_at": datetime.now(timezone.utc).isoformat(),
"last_updated_at": now_cn_aware().isoformat(),
}
@ -1806,7 +1807,7 @@ async def list_signals(
}
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(timezone.utc).replace(tzinfo=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,
@ -1911,7 +1912,7 @@ async def upload_signal_attachment(
"image_key": None,
"media_id": None,
"source": "manual_upload",
"occurred_at": datetime.now(timezone.utc).isoformat(),
"occurred_at": now_cn_aware().isoformat(),
"download_url": _build_signal_attachment_download_url(pid, sid, attachment_id),
}
@ -2918,7 +2919,7 @@ async def get_task_board(
risk_count = 0
if (t.status or "").lower() == "blocked":
risk_count += 1
if t.due_date and t.due_date < datetime.now(timezone.utc).replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES:
if t.due_date and t.due_date < now_cn_aware().replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES:
risk_count += 1
rows.append(
{
@ -2960,7 +2961,7 @@ async def patch_task_status(
before = {"status": task.status}
task.status = body.status
if body.status in DONE_STATUSES:
task.completed_at = datetime.now(timezone.utc).replace(tzinfo=None)
task.completed_at = now_cn_naive()
await log_operation(
db,
tenant_id=tenant_id,
@ -3420,7 +3421,7 @@ async def patch_release_status(
release_node.status = body.status
content = _as_dict(release_node.content)
content["status_note"] = body.note
content["last_status_changed_at"] = datetime.now(timezone.utc).isoformat()
content["last_status_changed_at"] = now_cn_aware().isoformat()
release_node.content = content
await log_operation(
db,
@ -3525,7 +3526,7 @@ async def patch_release_verification_item(
target["status"] = body.status
if body.status == "done":
target["verified_by"] = str(current_user.id)
target["verified_at"] = datetime.now(timezone.utc).isoformat()
target["verified_at"] = now_cn_aware().isoformat()
else:
target["verified_by"] = None
target["verified_at"] = None
@ -3574,7 +3575,7 @@ async def complete_release_verification(
incomplete = [item for item in items if str(item.get("status") or "").lower() != "done"]
if incomplete:
raise HTTPException(status_code=409, detail="仍有未完成验证项,请先逐项完成")
now_str = datetime.now(timezone.utc).isoformat()
now_str = now_cn_aware().isoformat()
content["verification_items"] = items
content["verification_done"] = True
content["verification_progress"] = {"done": len(items), "total": len(items)}
@ -3912,7 +3913,7 @@ async def archive_review(
review_node.status = "archived" if body.archive else "draft"
content = _as_dict(review_node.content)
content["archive_note"] = body.note
content["archived_at"] = datetime.now(timezone.utc).isoformat() if body.archive else None
content["archived_at"] = now_cn_aware().isoformat() if body.archive else None
review_node.content = content
await log_operation(
db,

View File

@ -43,6 +43,7 @@ from ..services.ai_pmo_kernel_service import AIPMOKernelService
from ..services.operation_log_service import log_operation
from ..services import skill_service
from ..utils.timezone import now_cn_naive
router = APIRouter(prefix="/users", tags=["users"])
COMPLETED_TASK_STATUSES = {"done", "completed", "closed", "resolved", "archived"}
@ -650,7 +651,7 @@ async def reset_password(
user.password_hash = AuthService.hash_password(body.new_password)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
user.updated_at = datetime.utcnow()
user.updated_at = now_cn_naive()
await log_operation(
db,
tenant_id=tenant_id,
@ -696,7 +697,7 @@ async def patch_user_status(
Task.assignee_id == user_id,
)
)
.values(assignee_id=None, updated_at=datetime.utcnow())
.values(assignee_id=None, updated_at=now_cn_naive())
)
workload_result = await db.execute(
select(UserWorkload).where(
@ -986,7 +987,7 @@ async def update_user_workload(
raise HTTPException(status_code=404, detail="User not found")
user.workload = max(0, min(200, int(workload)))
user.updated_at = datetime.utcnow()
user.updated_at = now_cn_naive()
await db.commit()
await db.refresh(user)
return _serialize_user(user)

View File

@ -13,6 +13,7 @@ from ..models import Signal
from ..services.llm import llm_service
from ..services.plane import plane_service
from ..utils.timezone import now_cn_aware
router = APIRouter(prefix="/webhooks", tags=["webhooks"])
@ -94,7 +95,7 @@ async def handle_git_webhook(
t["status"] = "Done"
t["audit_feedback"] = audit_result.get("reason")
t["completed_at"] = (
datetime.now(timezone.utc).isoformat()
now_cn_aware().isoformat()
)
target_signal.tasks_generated = json.dumps(tasks)
await db.commit()

View File

@ -12,6 +12,7 @@ import re
import logging
from pydantic import BaseModel, Field
from .utils.timezone import now_cn_aware
logger = logging.getLogger(__name__)
@ -356,7 +357,7 @@ class RoutingResult(BaseModel):
confidence: float
details: Dict[str, Any]
signal_id: Optional[str] = None
timestamp: float = Field(default_factory=lambda: datetime.utcnow().timestamp())
timestamp: float = Field(default_factory=lambda: now_cn_aware().timestamp())
# 全局路由器实例

View File

@ -12,6 +12,7 @@ import asyncio
import logging
from contextlib import asynccontextmanager
from .utils.timezone import now_cn_aware
logger = logging.getLogger(__name__)
@ -74,7 +75,7 @@ class Event:
type: EventType
data: Dict[str, Any]
timestamp: float = field(default_factory=lambda: datetime.utcnow().timestamp())
timestamp: float = field(default_factory=lambda: now_cn_aware().timestamp())
source: Optional[str] = None
correlation_id: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)

View File

@ -29,6 +29,7 @@ from .services.feishu_long_connection import init_feishu_long_connection_manager
from .services.feishu_integration import dispatch_pending_feishu_replies
from .services.context_cache import register_context_cache_invalidation_hooks
from .utils.timezone import now_cn_aware
configure_logging(settings.debug)
logger = get_logger(__name__)
register_context_cache_invalidation_hooks()
@ -422,7 +423,7 @@ async def _get_recent_active_projects(
limit_per_tenant: int,
max_projects: int,
) -> list[tuple[int, int]]:
since = (datetime.now(timezone.utc) - timedelta(days=max(1, int(days)))).replace(tzinfo=None)
since = (now_cn_aware() - timedelta(days=max(1, int(days)))).replace(tzinfo=None)
per_tenant_limit = max(1, int(limit_per_tenant))
total_limit = max(per_tenant_limit, int(max_projects))

View File

@ -54,6 +54,7 @@ from .signal_source_orchestration_service import SignalSourceOrchestrationServic
from .symbol_index_service import SymbolIndexService
from ..utils.timezone import CN_TZ, now_cn_aware, now_cn_naive
def _env_int(name: str, default: int, *, minimum: int, maximum: int) -> int:
raw = str(os.getenv(name, "") or "").strip()
if not raw:
@ -1408,7 +1409,7 @@ class AdaptiveRAGService:
"original_question": str(question or "")[:4000],
"normalized_question": summary,
"write_mode": "ack_first",
"created_at": datetime.now(timezone.utc).isoformat(),
"created_at": now_cn_aware().isoformat(),
}
signal = Signal(
source="PMFlow",
@ -2366,7 +2367,7 @@ class AdaptiveRAGService:
history = session_state.rewrite_history if isinstance(session_state.rewrite_history, list) else []
history.append(
{
"at": datetime.now(timezone.utc).isoformat(),
"at": now_cn_aware().isoformat(),
"question": question[:500],
"effective_question": effective_question[:500],
"query_type": str(query_type or QUERY_TYPE_UNKNOWN),
@ -3116,7 +3117,7 @@ class AdaptiveRAGService:
symbol_snapshot=symbol_snapshot,
kg_overview=kg_overview,
),
"generated_at": datetime.now(timezone.utc).isoformat(),
"generated_at": now_cn_aware().isoformat(),
}
async def entity_resolution(
@ -3516,7 +3517,7 @@ class AdaptiveRAGService:
"python_symbols": 0,
"typescript_symbols": 0,
"top_modules": [],
"generated_at": datetime.now(timezone.utc).isoformat(),
"generated_at": now_cn_aware().isoformat(),
}
candidates = [
@ -3568,7 +3569,7 @@ class AdaptiveRAGService:
"python_symbols": int(python_symbols),
"typescript_symbols": int(ts_symbols),
"top_modules": top_modules,
"generated_at": datetime.now(timezone.utc).isoformat(),
"generated_at": now_cn_aware().isoformat(),
}
@staticmethod
@ -4730,7 +4731,7 @@ class AdaptiveRAGService:
file_url=(file_url or "")[:1024] or None,
uploaded_by=uploaded_by,
status="processing",
last_fetched_at=datetime.now(timezone.utc) if source_type == "url" else None,
last_fetched_at=now_cn_naive() if source_type == "url" else None,
)
self.db.add(doc)
await self.db.commit()
@ -4814,7 +4815,7 @@ class AdaptiveRAGService:
doc.status = "complete"
doc.error_message = None
if source_type == "url":
doc.last_fetched_at = datetime.now(timezone.utc)
doc.last_fetched_at = now_cn_naive()
await self.db.commit()
except Exception as exc:
doc.status = "failed"
@ -4906,7 +4907,7 @@ class AdaptiveRAGService:
"sender": sender,
"chat_id": chat_id,
"message_id": message_id,
"captured_at": (captured_at or datetime.now(timezone.utc)).isoformat(),
"captured_at": (captured_at or now_cn_aware()).isoformat(),
},
)
)
@ -5071,7 +5072,7 @@ class AdaptiveRAGService:
"project_id": int(project_id) if project_id is not None else None,
"target_url": target_url,
"title": title[:255],
"updated_at": datetime.now(timezone.utc).isoformat(),
"updated_at": now_cn_aware().isoformat(),
},
)
)
@ -5143,7 +5144,7 @@ class AdaptiveRAGService:
"risk_level": risk,
"target_url": None,
"title": title,
"updated_at": datetime.now(timezone.utc).isoformat(),
"updated_at": now_cn_aware().isoformat(),
}
return record_id, title, content, metadata
@ -5268,7 +5269,7 @@ class AdaptiveRAGService:
return None
if value.tzinfo is None:
return value
return value.astimezone(timezone.utc).replace(tzinfo=None)
return value.astimezone(CN_TZ).replace(tzinfo=None)
@staticmethod
def _normalize_session_title_raw(text: str, *, max_len: int = 30) -> str:
@ -5305,7 +5306,7 @@ class AdaptiveRAGService:
if asked_by is None:
return (None, 1)
now_utc = datetime.now(timezone.utc).replace(tzinfo=None)
now_utc = now_cn_naive()
chosen: Optional[QASession] = None
preferred_id = int(preferred_session_id) if preferred_session_id else 0
if preferred_id > 0:

View File

@ -9,9 +9,9 @@ from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..models import AIOrchestrationRun
from ..utils.timezone import now_cn_naive
def _utc_now_naive() -> datetime:
return now_cn_naive()

View File

@ -9,9 +9,9 @@ from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..models import Budget, Notification, PortfolioSnapshot, Project, ProjectDomain, ProjectSignalLink, Signal, SignalSourceRegistry, Task, UserProject, UserWorkload, WorkItem
from ..utils.timezone import now_cn_naive
def _utc_now_naive() -> datetime:
return now_cn_naive()

View File

@ -72,8 +72,8 @@ from .openclaw_commander_service import OpenClawCommanderService
from .ai_pmo_domain_workflow import SignalSourceDomainWorkflow
from .llm import llm_service
from .signal_source_orchestration_service import SignalSourceOrchestrationService
from ..utils.timezone import now_cn_naive
from ..utils.timezone import now_cn_naive
logger = get_logger(__name__)
INTENT_QUERY = "query.read_only"
@ -103,6 +103,18 @@ RECEIPT_PHASE_ACTION = "action_applied"
RECEIPT_PHASE_PENDING = "pending_manual"
RECEIPT_PHASE_ERROR = "error"
def _looks_like_upstream_timeout_message(text: str) -> bool:
lowered = str(text or "").strip().lower()
if not lowered:
return False
timeout_signals = (
"request timed out before a response was generated",
"agents.defaults.timeoutseconds",
"timeoutseconds",
)
return any(signal in lowered for signal in timeout_signals)
QUESTION_HINTS = (
"?",
"?",
@ -1035,6 +1047,19 @@ class AIPMOKernelService:
project_hint_id: Optional[int] = None,
) -> dict[str, Any]:
started = time.perf_counter()
perf_work_item_id: Optional[int] = None
perf_intent_name = ""
def _log_perf(stage_name: str, stage_started_at: float) -> None:
elapsed_seconds = time.perf_counter() - stage_started_at
perf_line = (
f"[PERF] {stage_name} took {elapsed_seconds:.2f}s "
f"work_item={perf_work_item_id if perf_work_item_id is not None else 'N/A'} "
f"intent={perf_intent_name or 'N/A'}"
)
logger.info(perf_line)
print(perf_line, flush=True)
clean_text = self._normalize_text(message_text)
if not clean_text:
return {"handled": True, "status": "ignored", "classification": "empty", "direct_reply_required": False}
@ -1096,6 +1121,7 @@ class AIPMOKernelService:
source_message_id=synthetic_message_id,
owner_user_id=_safe_int(getattr(operator, "id", None)),
)
perf_work_item_id = int(work_item.id)
session = await self._get_or_create_session(tenant_id=self.tenant_id, sender=sender)
try:
@ -1106,6 +1132,7 @@ class AIPMOKernelService:
query_tables=world_model_query_tables,
session=session,
)
stage_started_at = time.perf_counter()
world_model = await self.build_world_model(
tenant_id=self.tenant_id,
project_id=_safe_int(getattr(project, "id", None)),
@ -1113,13 +1140,17 @@ class AIPMOKernelService:
project=project,
pre_queried_tables=world_model_query_tables,
)
_log_perf("_build_world_model", stage_started_at)
self._schedule_portfolio_snapshot_persist(world_model=world_model)
work_item.state = STATE_MODELED
stage_started_at = time.perf_counter()
intent, intent_context_source = await self._resolve_intent(
message_text=clean_text,
session=session,
world_model=world_model,
)
perf_intent_name = intent.name
_log_perf("_resolve_intent", stage_started_at)
if intent_context_source:
world_model["context_source"] = intent_context_source
work_item.context_snapshot = world_model
@ -1162,11 +1193,13 @@ class AIPMOKernelService:
work_item.intent = intent.name
work_item.project_id = intent.project_id
stage_started_at = time.perf_counter()
decision_payload = self.decision_engine.decide(
intent=intent,
world_model=world_model,
message_text=clean_text,
)
_log_perf("DecisionEngine.decide", stage_started_at)
correction_action = str((intent.metadata or {}).get("correction_action") or "none").strip() or "none"
requires_manual_review = bool((intent.metadata or {}).get("requires_manual_review"))
manual_review_code = str((intent.metadata or {}).get("manual_review_code") or "manual_review_required").strip()
@ -1291,7 +1324,8 @@ class AIPMOKernelService:
last_message_text=clean_text[:200],
)
await self.db.commit()
return self._build_capture_result(
stage_started_at = time.perf_counter()
capture_result = self._build_capture_result(
work_item=work_item,
intent=intent,
execution_result=work_item.execution_result or {},
@ -1300,11 +1334,14 @@ class AIPMOKernelService:
status="captured",
classification="command",
)
_log_perf("_build_capture_result", stage_started_at)
return capture_result
work_item.state = STATE_PLANNED
work_item.action_bundle = decision_payload["execution_plan"]
work_item.state = STATE_EXECUTING
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
stage_started_at = time.perf_counter()
execution_result, verification = await self._execute_followup_intent(
work_item=work_item,
intent=intent,
@ -1317,7 +1354,9 @@ class AIPMOKernelService:
source_sender_open_id=source_sender_open_id,
world_model=world_model,
)
_log_perf("TypedExecutor.execute", stage_started_at)
else:
stage_started_at = time.perf_counter()
execution_result = await self.executor.execute(
intent=intent,
message_text=clean_text,
@ -1326,6 +1365,7 @@ class AIPMOKernelService:
source_system=source_system,
source_message_id=synthetic_message_id,
)
_log_perf("TypedExecutor.execute", stage_started_at)
verification = await self._verify_execution(intent=intent, execution_result=execution_result, world_model=world_model)
if isinstance(execution_result, dict):
execution_result.setdefault("decision_trace", decision_trace)
@ -1399,7 +1439,8 @@ class AIPMOKernelService:
last_message_text=clean_text[:200],
)
await self.db.commit()
return self._build_capture_result(
stage_started_at = time.perf_counter()
capture_result = self._build_capture_result(
work_item=work_item,
intent=intent,
execution_result=execution_result,
@ -1412,6 +1453,8 @@ class AIPMOKernelService:
else ("signal" if intent.name == INTENT_SIGNAL_CREATE else "command")
),
)
_log_perf("_build_capture_result", stage_started_at)
return capture_result
except Exception as exc:
await self.db.rollback()
logger.warning(
@ -3160,6 +3203,17 @@ class AIPMOKernelService:
classification: str,
) -> dict[str, Any]:
linked_project_ids = [int(work_item.project_id)] if work_item.project_id is not None else []
direct_reply_text = str(render.get("text") or "").strip()
if _looks_like_upstream_timeout_message(direct_reply_text):
logger.warning(
"ai_pmo_upstream_timeout_reply_detected",
tenant_id=int(self.tenant_id),
work_item_id=int(work_item.id),
intent=str(intent.name or ""),
source_message_id=str(work_item.source_message_id or "")[:128],
observed_at=now_cn_naive().isoformat(),
reply_preview=direct_reply_text[:260],
)
result = {
"handled": True,
"status": status,
@ -3179,7 +3233,7 @@ class AIPMOKernelService:
],
"intent": intent.name,
"direct_reply_required": bool(direct_reply_required),
"direct_reply_text": str(render.get("text") or "").strip(),
"direct_reply_text": direct_reply_text,
"reply_template_key": str(render.get("template_key") or "").strip(),
"work_item_id": int(work_item.id),
"decision_level": work_item.decision_level,

View File

@ -18,6 +18,7 @@ from typing import Any, AsyncGenerator, Optional, Union
from sqlalchemy import and_, select
# LangChain & OpenAI
from ..utils.timezone import now_cn_naive
try:
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
@ -524,7 +525,7 @@ class AIService:
if b:
b.quality_score = min(100, max(0, (b.quality_score or 0) + quality_score))
b.efficiency_score = min(100, max(0, (b.efficiency_score or 0) + efficiency_score))
b.last_used_at = datetime.utcnow()
b.last_used_at = now_cn_naive()
await db.commit()
updates.append({
"user_id": str(uid),

View File

@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
from datetime import datetime
from enum import Enum
from ..utils.timezone import now_cn_aware
logger = logging.getLogger(__name__)
@ -45,7 +46,7 @@ class AuditResult(BaseModel):
issues: List[AuditIssue] = Field(default_factory=list)
summary: str = ""
details: Dict[str, Any] = Field(default_factory=dict)
timestamp: float = Field(default_factory=lambda: datetime.utcnow().timestamp())
timestamp: float = Field(default_factory=lambda: now_cn_aware().timestamp())
class SecurityIssue(BaseModel):

View File

@ -11,6 +11,7 @@ import hmac
import os
# Try to import passlib, fallback to simple hashing
from ..utils.timezone import now_cn_aware, now_cn_naive
try:
from passlib.context import CryptContext
from jose import JWTError, jwt
@ -106,15 +107,15 @@ class AuthService:
import json
token_data = {
**data,
"exp": (datetime.utcnow() + (expires_delta or timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS))).timestamp()
"exp": (now_cn_naive() + (expires_delta or timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS))).timestamp()
}
return base64.b64encode(json.dumps(token_data).encode()).decode()
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
expire = now_cn_naive() + expires_delta
else:
expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
expire = now_cn_naive() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@ -131,7 +132,7 @@ class AuthService:
import json
try:
token_data = json.loads(base64.b64decode(token).decode())
if token_data.get("exp", 0) > datetime.utcnow().timestamp():
if token_data.get("exp", 0) > now_cn_aware().timestamp():
return token_data.get("sub")
return None
except:

View File

@ -16,6 +16,7 @@ from datetime import datetime, timedelta
from enum import Enum
from ..events import Event, EventType
from ..utils.timezone import now_cn_aware, now_cn_naive
logger = logging.getLogger(__name__)
@ -138,12 +139,12 @@ class AutoMergeService:
# 4. 创建人工确认请求
if self.HUMAN_CONFIRMATION_REQUIRED:
confirmation_token = self._generate_confirmation_token()
expires_at = datetime.utcnow().timestamp() + self.CONFIRMATION_TIMEOUT
expires_at = now_cn_aware().timestamp() + self.CONFIRMATION_TIMEOUT
confirmation = ConfirmationRequest(
token=confirmation_token,
merge_request=request,
created_at=datetime.utcnow().timestamp(),
created_at=now_cn_aware().timestamp(),
expires_at=expires_at,
)
self._pending_confirmations[confirmation_token] = confirmation
@ -179,7 +180,7 @@ class AutoMergeService:
)
# 检查是否过期
if datetime.utcnow().timestamp() > confirmation.expires_at:
if now_cn_aware().timestamp() > confirmation.expires_at:
del self._pending_confirmations[confirmation_token]
return MergeResponse(
status=MergeStatus.REJECTED,
@ -401,7 +402,7 @@ class AutoMergeService:
if self._last_failure_time is None:
return False
elapsed = (datetime.utcnow() - self._last_failure_time).total_seconds()
elapsed = (now_cn_naive() - self._last_failure_time).total_seconds()
return elapsed < self.FAILURE_COOLDOWN_SECONDS
def _get_cooldown_remaining(self) -> float:
@ -409,7 +410,7 @@ class AutoMergeService:
if self._last_failure_time is None:
return 0.0
elapsed = (datetime.utcnow() - self._last_failure_time).total_seconds()
elapsed = (now_cn_naive() - self._last_failure_time).total_seconds()
return max(0, self.FAILURE_COOLDOWN_SECONDS - elapsed)
def _all_tests_passed(self, test_results: Dict[str, Any]) -> bool:
@ -448,7 +449,7 @@ class AutoMergeService:
def _record_failure(self):
"""记录失败"""
self._failure_count += 1
self._last_failure_time = datetime.utcnow()
self._last_failure_time = now_cn_naive()
# 检查是否需要进入熔断期
if self._failure_count >= self.MAX_CONSECUTIVE_FAILURES:
@ -478,7 +479,7 @@ class AutoMergeService:
async def get_pending_confirmations(self) -> List[ConfirmationRequest]:
"""获取待确认的请求"""
# 清理过期的确认请求
now = datetime.utcnow().timestamp()
now = now_cn_aware().timestamp()
expired = [
token for token, conf in self._pending_confirmations.items()
if now > conf.expires_at

View File

@ -30,6 +30,7 @@ from .llm import LLMService
# 阻塞模式正则表达式
from ..utils.timezone import now_cn_naive
BLOCKER_PATTERNS = [
r'等待.*接口',
r'等待.*API',
@ -222,7 +223,7 @@ class BlockerDetectionService:
raise ValueError(f"阻塞 {blocker_id} 不存在")
blocker.status = 'resolved'
blocker.resolved_at = datetime.now(timezone.utc)
blocker.resolved_at = now_cn_naive()
blocker.description += f"\n\n解决方案:{resolution_note}"
await self.db.commit()

View File

@ -18,6 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import Budget, BudgetAlert
from ..utils.timezone import now_cn_naive
class BudgetEnforcementService:
"""预算强制执行服务"""
@ -39,7 +40,7 @@ class BudgetEnforcementService:
"budget_checked": False,
}
now = datetime.utcnow()
now = now_cn_naive()
await self._ensure_current_period(budget, now)
current_usage = float(budget.current_usage_usd or 0.0)
@ -122,7 +123,7 @@ class BudgetEnforcementService:
if not budget:
return {"updated": False, "reason": "no_budget_configured"}
now = datetime.utcnow()
now = now_cn_naive()
await self._ensure_current_period(budget, now)
cost = max(float(actual_cost_usd or 0.0), 0.0)
@ -255,7 +256,7 @@ class BudgetEnforcementService:
if not budget:
raise ValueError(f"预算 {budget_id} 不存在")
now = datetime.utcnow()
now = now_cn_naive()
expires_at = now + timedelta(hours=max(duration_hours, 1))
history = dict(budget.override_history or {})
records = list(history.get("records", []))

View File

@ -15,6 +15,7 @@ from pydantic import BaseModel, Field
from datetime import datetime
from pathlib import Path
from ..utils.timezone import now_cn_aware
logger = logging.getLogger(__name__)
@ -91,7 +92,7 @@ class CodeDraftService:
"""
生成代码草案(激进策略)
"""
start_time = datetime.utcnow().timestamp()
start_time = now_cn_aware().timestamp()
try:
# 1. 读取目标文件
@ -130,7 +131,7 @@ class CodeDraftService:
# 10. 收集警告
warnings = self._collect_warnings(generated_files, file_contexts)
generation_time = datetime.utcnow().timestamp() - start_time
generation_time = now_cn_aware().timestamp() - start_time
return CodeDraftResponse(
success=True,
@ -158,7 +159,7 @@ class CodeDraftService:
confidence=0.0,
warnings=[f"生成过程出错: {str(e)}"],
review_points=[],
generation_time=datetime.utcnow().timestamp() - start_time,
generation_time=now_cn_aware().timestamp() - start_time,
)
async def _read_target_files(

View File

@ -15,6 +15,7 @@ from sqlalchemy.orm import Session
from ..models import Project, ProjectSignalLink, Signal, Task, User, UserProject, UserWorkload
from ..utils.timezone import now_cn_aware
try:
import redis.asyncio as redis_async
from redis import Redis as RedisSync
@ -178,7 +179,7 @@ class ContextCacheService:
"projects": projects.get("items", []),
"team_members": team,
"pending_signals": pending,
"snapshot_at": datetime.now(timezone.utc).isoformat(),
"snapshot_at": now_cn_aware().isoformat(),
}
@staticmethod
@ -558,7 +559,7 @@ class ContextCacheService:
"backend_root": str(backend_root),
"app_python_files": app_python_files,
"feature_catalog_source": str(feature_catalog.get("source") or "unknown"),
"scanned_at": datetime.now(timezone.utc).isoformat(),
"scanned_at": now_cn_aware().isoformat(),
},
}
@ -654,7 +655,7 @@ class ContextCacheService:
payload = {
"items": items,
"count": len(items),
"snapshot_at": datetime.now(timezone.utc).isoformat(),
"snapshot_at": now_cn_aware().isoformat(),
}
await self._set_json(key, payload, PROJECT_LIST_TTL_SECONDS)
return payload
@ -759,7 +760,7 @@ class ContextCacheService:
"p0_count": int(p0_count or 0),
"p1_count": int(p1_count or 0),
"p2_count": int(p2_count or 0),
"snapshot_at": datetime.now(timezone.utc).isoformat(),
"snapshot_at": now_cn_aware().isoformat(),
}
await self._set_json(key, payload, SIGNAL_PENDING_TTL_SECONDS)
return payload
@ -892,7 +893,7 @@ class ContextCacheService:
],
"pending_signals": pending_signals,
"workload_ratio": workload_ratio,
"snapshot_at": datetime.now(timezone.utc).isoformat(),
"snapshot_at": now_cn_aware().isoformat(),
}
async def get_session_messages(self, *, tenant_id: int, session_id: Optional[int]) -> list[dict[str, str]]:

View File

@ -28,6 +28,7 @@ from ..models import (
from .llm import LLMService
from ..utils.timezone import now_cn_aware
def _parse_involved_user_ids(raw_value: Any) -> List[int]:
"""兼容 involved_user_ids 的 list / json-string / 单值字符串。"""
if raw_value is None:
@ -86,7 +87,7 @@ class DailyBriefingService:
raise ValueError(f"User {user_id} not found")
# 计算时间范围
now = datetime.now(timezone.utc)
now = now_cn_aware()
yesterday = now - timedelta(days=1)
# 根据角色收集相关信息
@ -456,7 +457,7 @@ class DailyBriefingService:
return {
'project_id': project_id,
'project_name': project.title,
'generated_at': datetime.now(timezone.utc).isoformat(),
'generated_at': now_cn_aware().isoformat(),
'task_summary': {
'total': len(tasks),
'by_status': status_counts,

View File

@ -13,6 +13,7 @@ from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..utils.timezone import now_cn_naive
logger = logging.getLogger(__name__)
@ -216,7 +217,7 @@ class ProjectDomainService:
await self._clear_default_domains()
domain.is_default = request.is_default
domain.updated_at = datetime.utcnow()
domain.updated_at = now_cn_naive()
await self.db.commit()
await self.db.refresh(domain)
@ -308,7 +309,7 @@ class ProjectDomainService:
raise ValueError("Domain is not active")
# 更新最后同步时间
domain.last_synced_at = datetime.utcnow()
domain.last_synced_at = now_cn_naive()
await self.db.commit()
# 设置活动域

View File

@ -30,6 +30,7 @@ from .llm import LLMService
from ..events import EventType, Event, get_event_bus
from ..utils.timezone import now_cn_aware
class WorkflowEventType:
"""工作流事件类型"""
HIGH_IMPACT_SIGNAL = "high_impact_signal"
@ -92,7 +93,7 @@ class EventDrivenWorkflowEngine:
{
'workflow_type': event_type,
'result': result,
'timestamp': datetime.now(timezone.utc).isoformat()
'timestamp': now_cn_aware().isoformat()
}
)

View File

@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import Experiment, ExperimentTruthNodeSync, TruthNode
from ..utils.timezone import now_cn_aware, now_cn_naive
class ExperimentSyncService:
"""实验结果 -> 宪章同步,支持自动应用、人工审核与回滚。"""
@ -147,7 +148,7 @@ class ExperimentSyncService:
return {"sync_id": record.id, "apply_status": record.apply_status}
record.reviewed_by = reviewer_id
record.reviewed_at = datetime.now(timezone.utc)
record.reviewed_at = now_cn_naive()
record.review_notes = review_notes
if action == "approve":
@ -192,7 +193,7 @@ class ExperimentSyncService:
record.apply_status = "requires_review"
record.reviewed_by = reviewer_id
record.reviewed_at = datetime.now(timezone.utc)
record.reviewed_at = now_cn_naive()
record.review_notes = rollback_notes or "manual rollback"
await self.db.commit()
@ -293,7 +294,7 @@ class ExperimentSyncService:
"sync_type": record.sync_type,
"confidence_score": record.confidence_score,
"experiment_summary": (record.sync_data or {}).get("experiment_summary", {}),
"applied_at": datetime.now(timezone.utc).isoformat(),
"applied_at": now_cn_aware().isoformat(),
}
content["experiment_sync"] = sync_section
truth_node.content = content

View File

@ -41,10 +41,12 @@ from .notification_service import NotificationService
from .signal_capture import ProactiveSignalCaptureService
from .llm import LLMService
from ..utils.timezone import now_cn_naive
CN_TZ = timezone(timedelta(hours=8))
WEEKDAY_CN = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
OUTBOX_MAX_ATTEMPTS = 3
OUTBOX_STALE_SENDING_SECONDS = 120
_OUTBOX_PERF_T0_BY_ID: dict[int, float] = {}
class FeishuIntegrationService:
@ -213,6 +215,15 @@ class FeishuIntegrationService:
chat = event.get('chat', {}) if isinstance(event.get('chat', {}), dict) else {}
sender = event.get('sender', {}) if isinstance(event.get('sender', {}), dict) else {}
message_id = (message.get("message_id") or "").strip() or "unknown"
perf_t0_raw = event.get("_perf_t0")
perf_t0: Optional[float] = None
if isinstance(perf_t0_raw, (int, float)):
perf_t0 = float(perf_t0_raw)
else:
try:
perf_t0 = float(str(perf_t0_raw).strip())
except Exception:
perf_t0 = None
self._emit_state_transition(
trace_id=state_trace_id,
state="RECEIVED",
@ -254,6 +265,7 @@ class FeishuIntegrationService:
text="我已收到你的消息,但目前无法直接解析纯图片内容。请补充 1-2 句文字需求,我会立即给出分派建议。",
trace_id=state_trace_id,
source_message_id=message_id,
perf_t0=perf_t0,
)
self._emit_state_transition(
trace_id=state_trace_id,
@ -318,6 +330,7 @@ class FeishuIntegrationService:
text=self._build_error_reply_text(),
trace_id=state_trace_id,
source_message_id=message_id,
perf_t0=perf_t0,
)
self._emit_state_transition(
trace_id=state_trace_id,
@ -401,6 +414,7 @@ class FeishuIntegrationService:
text=deterministic_reply,
trace_id=state_trace_id,
source_message_id=message_id,
perf_t0=perf_t0,
)
reply_route = "execute_template"
elif scene == "qa":
@ -417,6 +431,7 @@ class FeishuIntegrationService:
text=str(result.get("direct_reply_text") or "已收到,正在处理。"),
trace_id=state_trace_id,
source_message_id=message_id,
perf_t0=perf_t0,
)
reply_route = "qa_direct"
elif (
@ -434,6 +449,7 @@ class FeishuIntegrationService:
capture_result=result,
trace_id=state_trace_id,
source_message_id=message_id,
perf_t0=perf_t0,
)
reply_route = "qa_llm"
else:
@ -451,6 +467,7 @@ class FeishuIntegrationService:
text=self._build_error_reply_text(),
trace_id=state_trace_id,
source_message_id=message_id,
perf_t0=perf_t0,
)
reply_route = "error_template"
@ -589,6 +606,7 @@ class FeishuIntegrationService:
capture_result: Dict[str, Any],
trace_id: Optional[str] = None,
source_message_id: Optional[str] = None,
perf_t0: Optional[float] = None,
) -> None:
"""被 @ 时回复一条有人味的执行反馈。"""
deterministic_time_reply = self._build_time_truth_reply(message_content)
@ -600,6 +618,7 @@ class FeishuIntegrationService:
text=deterministic_time_reply,
trace_id=trace_id,
source_message_id=source_message_id,
perf_t0=perf_t0,
)
return
@ -619,6 +638,7 @@ class FeishuIntegrationService:
text=execution_receipt,
trace_id=trace_id,
source_message_id=source_message_id,
perf_t0=perf_t0,
)
return
@ -631,6 +651,7 @@ class FeishuIntegrationService:
text=butler_reply,
trace_id=trace_id,
source_message_id=source_message_id,
perf_t0=perf_t0,
)
return
@ -683,6 +704,7 @@ class FeishuIntegrationService:
text=reply_text,
trace_id=trace_id,
source_message_id=source_message_id,
perf_t0=perf_t0,
)
async def _safe_reply_text(
@ -694,6 +716,7 @@ class FeishuIntegrationService:
mention_name: Optional[str] = None,
trace_id: Optional[str] = None,
source_message_id: Optional[str] = None,
perf_t0: Optional[float] = None,
) -> None:
try:
plain_text = self._normalize_outgoing_reply_text(text)
@ -702,12 +725,27 @@ class FeishuIntegrationService:
display_name=mention_name,
)
final_text = f"{mention_prefix}{plain_text}".strip()
await self._enqueue_reply_outbox(
outbox_id = await self._enqueue_reply_outbox(
chat_id=chat_id,
message_text=final_text,
trace_id=trace_id,
source_message_id=source_message_id,
)
perf_t1 = time.perf_counter()
if outbox_id > 0 and perf_t0 is not None:
_OUTBOX_PERF_T0_BY_ID[int(outbox_id)] = float(perf_t0)
if perf_t0 is not None:
print(
f"[PERF] outbox_written ts={now_cn_naive().isoformat()} T1={perf_t1:.6f} "
f"delta={perf_t1 - perf_t0:.2f}s "
f"outbox_id={outbox_id}"
)
else:
print(
f"[PERF] outbox_written ts={now_cn_naive().isoformat()} T1={perf_t1:.6f} "
f"delta=N/A "
f"outbox_id={outbox_id}"
)
except Exception as exc:
print(f"[Feishu] enqueue reply outbox failed: chat_id={chat_id}, error={exc}")
# 统一由 drain_loop 消费 outbox,禁止业务路径直发,避免双发送。
@ -783,7 +821,7 @@ class FeishuIntegrationService:
fetch_limit = max(1, min(200, int(limit)))
dispatched = 0
for _ in range(fetch_limit):
claimed = await cls._claim_next_outbox_entry(now=datetime.utcnow())
claimed = await cls._claim_next_outbox_entry(now=now_cn_naive())
if claimed is None:
break
await cls._dispatch_claimed_outbox_entry(claimed=claimed)
@ -883,10 +921,11 @@ class FeishuIntegrationService:
).scalar_one_or_none()
if row is not None and str(row.status or "").lower() == "sending":
row.status = "sent"
row.sent_at = datetime.utcnow()
row.sent_at = now_cn_naive()
row.next_retry_at = None
row.last_error = f"dedupe_skip_sent:{int(sent_duplicate)}"
await db_skip.commit()
_OUTBOX_PERF_T0_BY_ID.pop(outbox_id, None)
return
send_error: Optional[str] = None
@ -914,18 +953,33 @@ class FeishuIntegrationService:
attempts = int(row.attempts or claimed.get("attempts") or 0)
if send_error is None:
row.status = "sent"
row.sent_at = datetime.utcnow()
row.sent_at = now_cn_naive()
row.next_retry_at = None
row.last_error = None
await db_update.commit()
perf_t2 = time.perf_counter()
perf_t0 = _OUTBOX_PERF_T0_BY_ID.pop(outbox_id, None)
if perf_t0 is not None:
print(
f"[PERF] reply_dispatched ts={now_cn_naive().isoformat()} T2={perf_t2:.6f} "
f"delta={perf_t2 - perf_t0:.2f}s "
f"outbox_id={outbox_id}"
)
else:
print(
f"[PERF] reply_dispatched ts={now_cn_naive().isoformat()} T2={perf_t2:.6f} "
f"delta=N/A "
f"outbox_id={outbox_id}"
)
return
if attempts >= OUTBOX_MAX_ATTEMPTS:
row.status = "failed"
row.next_retry_at = None
_OUTBOX_PERF_T0_BY_ID.pop(outbox_id, None)
else:
row.status = "retrying"
delay_seconds = min(30, 2 * (2 ** max(0, attempts - 1)))
row.next_retry_at = datetime.utcnow() + timedelta(seconds=delay_seconds)
row.next_retry_at = now_cn_naive() + timedelta(seconds=delay_seconds)
row.last_error = (send_error or "unknown_send_error")[:1000]
await db_update.commit()
@ -1466,7 +1520,7 @@ class FeishuIntegrationService:
ts = int(str(raw).strip())
if ts > 10_000_000_000:
ts = ts / 1000.0
return datetime.fromtimestamp(ts, tz=timezone.utc)
return datetime.fromtimestamp(ts, tz=CN_TZ)
except Exception:
return None

View File

@ -18,6 +18,7 @@ from ..core.logging import get_logger
from ..models import Integration
from .feishu_integration import FeishuIntegrationService
from ..utils.timezone import CN_TZ, now_cn_aware
logger = get_logger(__name__)
DEFAULT_FEISHU_LONG_CONNECTION_RETRY_MAX_ATTEMPTS = 3
DEFAULT_FEISHU_LONG_CONNECTION_RETRY_BASE_SECONDS = 1.0
@ -144,7 +145,7 @@ class FeishuLongConnectionManager:
"last_disconnect_at": self._last_disconnect_at,
"restart_attempts": self._restart_attempts,
"next_retry_at": (
datetime.fromtimestamp(self._next_retry_at, tz=timezone.utc).isoformat()
datetime.fromtimestamp(self._next_retry_at, tz=CN_TZ).isoformat()
if self._next_retry_at > 0
else None
),
@ -249,7 +250,7 @@ class FeishuLongConnectionManager:
try:
await self._preflight_auth(tenant_id=self._tenant_id)
self._last_auth_at = datetime.now(timezone.utc).isoformat()
self._last_auth_at = now_cn_aware().isoformat()
except Exception as exc:
code, retryable = self._classify_error(exc)
self._set_error(str(exc), code=code, retryable=retryable)
@ -266,7 +267,7 @@ class FeishuLongConnectionManager:
return False
self._clear_error()
self._last_connect_at = datetime.now(timezone.utc).isoformat()
self._last_connect_at = now_cn_aware().isoformat()
self._last_disconnect_at = None
self._restart_attempts = 0
self._next_retry_at = 0.0
@ -403,7 +404,7 @@ class FeishuLongConnectionManager:
logger.warning(f"feishu_long_connection_client_cleanup_failed: {exc}")
self._client = None
if self._running and not self._stopping:
self._last_disconnect_at = datetime.now(timezone.utc).isoformat()
self._last_disconnect_at = now_cn_aware().isoformat()
if not self._last_error_code:
self._set_error("feishu long connection thread exited", code="thread_exited", retryable=True)
self._schedule_retry()
@ -416,6 +417,7 @@ class FeishuLongConnectionManager:
return
try:
perf_t0 = time.perf_counter()
payload = self._to_dict(data)
message_id = ""
event_type = ""
@ -432,6 +434,13 @@ class FeishuLongConnectionManager:
).strip()
except Exception:
pass
if isinstance(payload, dict):
payload["_perf_t0"] = float(perf_t0)
print(
f"[PERF] message_received ts={now_cn_aware().isoformat()} T0={perf_t0:.6f} "
f"message_id={message_id or 'unknown'} "
f"event_type={event_type or 'unknown'}"
)
logger.info(
"feishu_long_connection_event_received: "
f"event_type={event_type or 'unknown'}, message_id={message_id or 'unknown'}"
@ -468,7 +477,7 @@ class FeishuLongConnectionManager:
try:
await self._process_payload(payload)
self._last_event_at = datetime.now(timezone.utc).isoformat()
self._last_event_at = now_cn_aware().isoformat()
except Exception as exc:
self._set_error(str(exc), code="event_process_failed", retryable=True)
logger.warning(f"feishu_long_connection_process_failed: {exc}")
@ -506,6 +515,10 @@ class FeishuLongConnectionManager:
if event_type:
event_data = {**event_data, "type": event_type}
perf_t0 = payload.get("_perf_t0")
if isinstance(perf_t0, (int, float)):
event_data = {**event_data, "_perf_t0": float(perf_t0)}
if event_data:
event_data = {**event_data, "_ingress_source": "long_connection"}

View File

@ -42,8 +42,8 @@ from .notification_service import NotificationService
from .openclaw_commander_service import OpenClawCommanderService, OperationExecutorService
from ..utils.project_members import ensure_project_default_members
from ..utils.sequence import ensure_project_id_sequence
from ..utils.timezone import CN_TZ, now_cn_aware
from ..utils.timezone import CN_TZ, now_cn_aware
logger = get_logger(__name__)
PROJECT_PRIORITY_ORDER = {
@ -4048,7 +4048,7 @@ class FeishuSignalPipelineService:
pending_rows = self._dedupe_queue_items(pending_rows)
pending_rows = [row for row in pending_rows if not self._should_hide_pending_item_from_inbox(row)]
# DB 列是 timestamp without time zone,比较值必须使用 naive UTC,避免 asyncpg aware/naive 报错。
# DB 列是 timestamp without time zone,比较值必须使用 naive 北京时间,避免 asyncpg aware/naive 报错。
today_start = _utc_now().astimezone(CN_TZ).replace(
hour=0,
minute=0,

View File

@ -16,6 +16,7 @@ from datetime import datetime, timedelta
from enum import Enum
import httpx
from ..utils.timezone import now_cn_naive
logger = logging.getLogger(__name__)
@ -118,12 +119,12 @@ class IntegrationService:
IntegrationType.PLANE: IntegrationHealth(
type=IntegrationType.PLANE,
status=IntegrationStatus.PENDING,
last_check=datetime.utcnow(),
last_check=now_cn_naive(),
),
IntegrationType.LARK: IntegrationHealth(
type=IntegrationType.LARK,
status=IntegrationStatus.PENDING,
last_check=datetime.utcnow(),
last_check=now_cn_naive(),
),
}
@ -180,7 +181,7 @@ class IntegrationService:
errors=[f"Plane integration is {health.status.value}"],
)
start_time = datetime.utcnow()
start_time = now_cn_naive()
try:
# 获取 Plane 服务
@ -198,7 +199,7 @@ class IntegrationService:
await self._record_failure(integration_type, ", ".join(result.get("errors", [])))
# 计算延迟
latency = (datetime.utcnow() - start_time).total_seconds() * 1000
latency = (now_cn_naive() - start_time).total_seconds() * 1000
health.latency_ms = latency
return TaskSyncResult(
@ -245,7 +246,7 @@ class IntegrationService:
error=f"Lark integration is {health.status.value}",
)
start_time = datetime.utcnow()
start_time = now_cn_naive()
try:
# 获取飞书服务
@ -259,7 +260,7 @@ class IntegrationService:
await self._record_success(integration_type)
# 计算延迟
latency = (datetime.utcnow() - start_time).total_seconds() * 1000
latency = (now_cn_naive() - start_time).total_seconds() * 1000
health.latency_ms = latency
# 发送事件
@ -384,12 +385,12 @@ class IntegrationService:
"""检查 Plane 集成健康"""
integration_type = IntegrationType.PLANE
health = self._health_status[integration_type]
start_time = datetime.utcnow()
start_time = now_cn_naive()
if not self.config.plane.enabled or not self.config.plane.api_key:
health.status = IntegrationStatus.INACTIVE
health.message = "Plane integration is disabled"
health.last_check = datetime.utcnow()
health.last_check = now_cn_naive()
return health
try:
@ -399,14 +400,14 @@ class IntegrationService:
health.status = IntegrationStatus.ACTIVE
health.message = "Connected"
health.last_check = datetime.utcnow()
health.latency_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
health.last_check = now_cn_naive()
health.latency_ms = (now_cn_naive() - start_time).total_seconds() * 1000
except Exception as e:
logger.warning(f"[Integration] Plane health check failed: {e}")
health.status = IntegrationStatus.ERROR
health.message = f"Health check failed: {str(e)}"
health.last_check = datetime.utcnow()
health.last_check = now_cn_naive()
return health
@ -414,12 +415,12 @@ class IntegrationService:
"""检查飞书集成健康"""
integration_type = IntegrationType.LARK
health = self._health_status[integration_type]
start_time = datetime.utcnow()
start_time = now_cn_naive()
if not self.config.lark.enabled or not self.config.lark.app_id:
health.status = IntegrationStatus.INACTIVE
health.message = "Lark integration is disabled"
health.last_check = datetime.utcnow()
health.last_check = now_cn_naive()
return health
try:
@ -434,14 +435,14 @@ class IntegrationService:
health.status = IntegrationStatus.DEGRADED
health.message = "Running in simulation mode"
health.last_check = datetime.utcnow()
health.latency_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
health.last_check = now_cn_naive()
health.latency_ms = (now_cn_naive() - start_time).total_seconds() * 1000
except Exception as e:
logger.warning(f"[Integration] Lark health check failed: {e}")
health.status = IntegrationStatus.ERROR
health.message = f"Health check failed: {str(e)}"
health.last_check = datetime.utcnow()
health.last_check = now_cn_naive()
return health
@ -494,7 +495,7 @@ class IntegrationService:
async def _record_success(self, integration_type: IntegrationType) -> None:
"""记录成功操作"""
health = self._health_status[integration_type]
health.last_success = datetime.utcnow()
health.last_success = now_cn_naive()
health.consecutive_failures = 0
if health.status == IntegrationStatus.ERROR:
health.status = IntegrationStatus.ACTIVE
@ -505,7 +506,7 @@ class IntegrationService:
health.last_error = error
health.error_count += 1
health.consecutive_failures += 1
health.last_check = datetime.utcnow()
health.last_check = now_cn_naive()
# 达到降级阈值
if health.consecutive_failures >= self.MAX_CONSECUTIVE_FAILURES:

View File

@ -31,6 +31,7 @@ from ..config.ai_prompts import build_default_role_prompt_bundle
from ..database import AsyncSessionLocal
from ..models import AICallLog, Integration
from ..utils.timezone import now_cn_aware
try:
import redis.asyncio as redis_async
@ -290,10 +291,28 @@ def _cn_now_text() -> str:
return datetime.now(cn_tz).strftime("%Y-%m-%d %H:%M:%S")
def _cn_now_text_ms() -> str:
cn_tz = timezone(timedelta(hours=8))
return datetime.now(cn_tz).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
def _runtime_log(scope: str, message: str) -> None:
print(f"[{_cn_now_text()}][{scope}] {message}")
def _looks_like_openclaw_timeout_message(text: str) -> bool:
lowered = str(text or "").strip().lower()
if not lowered:
return False
timeout_signals = (
"request timed out before a response was generated",
"agents.defaults.timeoutseconds",
"timeoutseconds",
"before a response was generated",
)
return any(signal in lowered for signal in timeout_signals)
@dataclass
class ChatCompletionResult:
text: str
@ -460,7 +479,7 @@ class LLMService:
raise ValueError("api_key 不能为空")
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
now = datetime.now(timezone.utc).isoformat()
now = now_cn_aware().isoformat()
engine_id = str(uuid4())
defaults = PROVIDER_DEFAULTS[provider]
engine = {
@ -521,7 +540,7 @@ class LLMService:
if base_url is not None:
target["base_url"] = (base_url or "").strip().rstrip("/") or defaults["base_url"]
target["updated_at"] = datetime.now(timezone.utc).isoformat()
target["updated_at"] = now_cn_aware().isoformat()
integration.config = json.loads(json.dumps(bundle))
await db.commit()
return self._sanitize_engine_for_response(target)
@ -1101,6 +1120,21 @@ class LLMService:
if cleaned_system_prompt:
messages.append({"role": "system", "content": cleaned_system_prompt})
messages.append({"role": "user", "content": prompt})
request_trace_id = uuid4().hex[:12]
request_started_at_perf = time.perf_counter()
function_label = str(function_name or "llm_generate")
openclaw_agent_timeout = int(openclaw_agents_timeout_seconds) if is_openclaw_gateway_request else None
if is_openclaw_gateway_request:
_runtime_log(
"OpenClaw调用",
(
f"start ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} provider={provider_name} model={model} "
f"request_timeout={float(request_timeout):.2f}s "
f"agent_timeout={openclaw_agent_timeout if openclaw_agent_timeout is not None else 'N/A'}"
),
)
try:
if provider_name == "ollama":
@ -1150,7 +1184,26 @@ class LLMService:
}
async with httpx.AsyncClient(timeout=request_timeout, trust_env=False) as client:
resp = await client.post(endpoint, headers=headers, json=payload)
resp.raise_for_status()
if is_openclaw_gateway_request:
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw调用",
(
f"http ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} status={resp.status_code} elapsed_ms={elapsed_ms:.0f}"
),
)
if resp.status_code >= 400:
body_preview = str(resp.text or "").replace("\n", " ").strip()[:260]
if is_openclaw_gateway_request:
_runtime_log(
"OpenClaw调用",
(
f"http_error ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} status={resp.status_code} body={body_preview}"
),
)
resp.raise_for_status()
data = resp.json()
choices = data.get("choices") or []
if not choices:
@ -1174,10 +1227,72 @@ class LLMService:
"LLM 返回空内容: "
f"message_keys={msg_keys}, choice_keys={choice_keys}, choice_preview={choice_preview}"
)
if _looks_like_openclaw_timeout_message(text):
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw超时" if is_openclaw_gateway_request else "LLM超时文本",
(
f"detected ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} elapsed_ms={elapsed_ms:.0f} "
f"text={text.replace(chr(10), ' ')[:260]}"
),
)
logger.warning(
"llm_timeout_payload_detected",
trace_id=request_trace_id,
function_name=function_label,
provider=provider_name,
model=model,
base_url=str(base_url or "")[:120],
request_timeout_seconds=float(request_timeout),
agent_timeout_seconds=openclaw_agent_timeout,
text_preview=text.replace("\n", " ")[:260],
)
if is_openclaw_gateway_request:
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw调用",
(
f"end ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} elapsed_ms={elapsed_ms:.0f} text_len={len(text)}"
),
)
if use_provider_circuit_breaker:
self._record_provider_success(circuit_key)
return ChatCompletionResult(text=text, raw_response=data)
except Exception as exc:
if _looks_like_openclaw_timeout_message(str(exc)):
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"LLM超时异常",
(
f"detected ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} elapsed_ms={elapsed_ms:.0f} "
f"error={type(exc).__name__}:{str(exc)[:260]}"
),
)
logger.warning(
"llm_timeout_exception_detected",
trace_id=request_trace_id,
function_name=function_label,
provider=provider_name,
model=model,
base_url=str(base_url or "")[:120],
request_timeout_seconds=float(request_timeout),
agent_timeout_seconds=openclaw_agent_timeout,
error_type=type(exc).__name__,
error=str(exc)[:260],
)
if is_openclaw_gateway_request:
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw调用",
(
f"exception ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} elapsed_ms={elapsed_ms:.0f} "
f"error={type(exc).__name__}:{str(exc)[:260]}"
),
)
if use_provider_circuit_breaker:
self._record_provider_failure(circuit_key, exc)
raise

View File

@ -26,6 +26,7 @@ from ..models import (
from ..services.ai_service import AIService
from ..utils.timezone import now_cn_naive
class ModelCapabilityService:
"""模型能力矩阵核心服务"""
@ -649,7 +650,7 @@ class ModelCapabilityService:
raise ValueError(f"能力差距 {gap_id} 不存在")
gap.status = "resolved"
gap.resolved_at = datetime.utcnow()
gap.resolved_at = now_cn_naive()
gap.resolution_notes = resolution_notes
await self.db.commit()

View File

@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import Notification, User
from ..utils.timezone import CN_TZ, now_cn_aware
class NotificationService:
def __init__(self, db: AsyncSession):
self.db = db
@ -215,9 +216,9 @@ class NotificationService:
@staticmethod
def _format_cn_time(dt: Optional[datetime]) -> str:
if not dt:
dt = datetime.now(timezone.utc)
dt = now_cn_aware()
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.replace(tzinfo=CN_TZ)
cn_tz = timezone(timedelta(hours=8))
return dt.astimezone(cn_tz).strftime("%Y-%m-%d %H:%M:%S")

View File

@ -41,9 +41,9 @@ from .sstc_service import ALIGNED_STATUSES
from .task_persistence import build_task_create_draft, persist_task
from ..utils.project_members import ensure_project_default_members
from ..utils.sequence import ensure_project_id_sequence
from ..utils.timezone import now_cn_naive
from ..utils.title import compact_title
from ..utils.timezone import now_cn_naive
SUPPORTED_COMMANDER_OPERATION_CODES = (
"project.create",
"project.update",
@ -2025,7 +2025,7 @@ class OperationExecutorService:
task.status = status
if status in DONE_TASK_STATUSES:
task.completed_at = datetime.now(timezone.utc).replace(tzinfo=None)
task.completed_at = now_cn_naive()
await log_operation(
self.db,

View File

@ -32,11 +32,11 @@ from ..models import (
Task,
User,
)
from ..utils.timezone import now_cn_naive
from .context_cache import context_cache_service
from .symbol_index_service import SymbolIndexService
from ..utils.timezone import now_cn_naive
def _utc_now_naive() -> datetime:
return now_cn_naive()

View File

@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..config.ai_prompts import build_default_role_prompt_bundle
from ..models import Integration
from ..utils.timezone import now_cn_aware
INTEGRATION_TYPE = "ai_prompt_profiles"
ROLE_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{1,31}$")
MAX_ROLES = 12
@ -35,7 +36,7 @@ def _safe_json(value: Any) -> dict[str, Any]:
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
return now_cn_aware().isoformat()
class PromptProfileService:

View File

@ -46,6 +46,7 @@ from ..utils.project_members import ensure_project_default_members
from ..utils.sequence import ensure_project_id_sequence, ensure_signal_id_sequence
from ..utils.timezone import CN_TZ, now_cn_aware
EMOJI_UNICODE_RE = re.compile(
r"[\U0001F300-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF"
r"\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FAFF"
@ -196,9 +197,9 @@ class ProactiveSignalCaptureService:
- 分享技术文章
"""
trace_id = f"feishu-{uuid4().hex[:12]}"
captured_dt = captured_at or datetime.now(timezone.utc)
captured_dt = captured_at or now_cn_aware()
if captured_dt.tzinfo is None:
captured_dt = captured_dt.replace(tzinfo=timezone.utc)
captured_dt = captured_dt.replace(tzinfo=CN_TZ)
proposer_name = self._normalize_single_line_text(sender or "未知用户", max_len=80) or "未知用户"
proposer_open_id = self._normalize_single_line_text(proposer_open_id or "", max_len=128) or None
@ -803,7 +804,7 @@ class ProactiveSignalCaptureService:
processed=True,
strategic_analysis=json.dumps({
'url': url,
'captured_at': datetime.now(timezone.utc).isoformat(),
'captured_at': now_cn_aware().isoformat(),
'user_id': user_id,
'ai_analysis': analysis.get('analysis', {})
}),
@ -862,7 +863,7 @@ class ProactiveSignalCaptureService:
'repo_url': repo_url,
'event_type': event_type,
'event_data': event_data,
'captured_at': datetime.now(timezone.utc).isoformat(),
'captured_at': now_cn_aware().isoformat(),
'ai_analysis': analysis.get('analysis', {})
}),
tasks_generated=json.dumps([]),
@ -922,7 +923,7 @@ class ProactiveSignalCaptureService:
strategic_analysis=json.dumps({
'hn_url': story.get('url'),
'hn_points': story.get('points'),
'captured_at': datetime.now(timezone.utc).isoformat(),
'captured_at': now_cn_aware().isoformat(),
'ai_analysis': analysis.get('analysis', {})
}),
tasks_generated=json.dumps(analysis.get('suggested_tasks', [])),
@ -970,7 +971,7 @@ class ProactiveSignalCaptureService:
logger.info("signal_capture_ai_think", stage=stage, **safe_payload)
try:
stage_zh = AI_THINK_STAGE_ZH.get(stage, stage)
now_text = self._format_cn_time(datetime.now(timezone.utc))
now_text = self._format_cn_time(now_cn_aware())
print(
f"[{now_text}][AI思考][SignalCapture] {stage_zh} "
+ json.dumps({"stage": stage, **safe_payload}, ensure_ascii=False)
@ -1851,7 +1852,7 @@ class ProactiveSignalCaptureService:
def _format_cn_time(dt: datetime) -> str:
safe_dt = dt
if safe_dt.tzinfo is None:
safe_dt = safe_dt.replace(tzinfo=timezone.utc)
safe_dt = safe_dt.replace(tzinfo=CN_TZ)
cn_tz = timezone(timedelta(hours=8))
return safe_dt.astimezone(cn_tz).strftime("%Y-%m-%d %H:%M:%S")
@ -2135,9 +2136,9 @@ URL: {url}
requested_at: Optional[datetime] = None,
) -> Optional[int]:
"""当分类为 new_project 时,自动创建项目并关联当前信号。"""
requested_dt = requested_at or datetime.now(timezone.utc)
requested_dt = requested_at or now_cn_aware()
if requested_dt.tzinfo is None:
requested_dt = requested_dt.replace(tzinfo=timezone.utc)
requested_dt = requested_dt.replace(tzinfo=CN_TZ)
project_brief = await self._draft_new_project_brief(
message=message,
tenant_id=tenant_id,

View File

@ -28,11 +28,11 @@ from ..models import (
User,
)
from ..database import AsyncSessionLocal
from ..utils.timezone import now_cn_naive
from .ai_orchestration_run_service import AIOrchestrationRunService
from .prompt_profile_service import prompt_profile_service
from .llm import llm_service
from ..utils.timezone import now_cn_naive
SOURCE_ONBOARDING_ACTION_RE = re.compile(
r"(新增|增加|添加|接入|配置|注册|建立|加个|加一个|加).{0,8}(信号源|来源|监控源|channel|source|webhook|飞书|github|url|rss)",
re.IGNORECASE,

View File

@ -14,6 +14,7 @@ from ..database import AsyncSessionLocal
from ..models import AICallLog, Task, UserSkillBinding
from .llm import llm_service
from ..utils.timezone import now_cn_naive
PRIORITY_WEIGHT = {
"critical": 1.5,
"high": 1.2,
@ -117,8 +118,8 @@ async def _apply_skill_deltas(
float(row.efficiency_score or 0.0) * 0.5
)
row.proficiency = int(_clamp(round(combined), 0, 100))
row.last_used_at = datetime.utcnow()
row.updated_at = datetime.utcnow()
row.last_used_at = now_cn_naive()
row.updated_at = now_cn_naive()
touched += 1
return touched

View File

@ -21,6 +21,7 @@ from .ai_pmo_kernel_service import AIPMOKernelService
# 系统预设 AI 开发专属技能标签
from ..utils.timezone import now_cn_naive
SYSTEM_SKILL_TAGS = [
{"name": "Python", "slug": "python", "category": "language", "sort_order": 1},
{"name": "TypeScript", "slug": "typescript", "category": "language", "sort_order": 2},
@ -196,7 +197,7 @@ async def update_skill_tag(
tag.description = description
if sort_order is not None:
tag.sort_order = sort_order
tag.updated_at = datetime.utcnow()
tag.updated_at = now_cn_naive()
await db.commit()
await db.refresh(tag)
return tag

View File

@ -22,6 +22,7 @@ from ..models import (
)
from ..utils.project_members import ensure_project_default_members
from ..utils.timezone import now_cn_aware
DONE_STATUSES = {"done", "completed"}
ACTIVE_STATUSES = {"in_progress", "review"}
BLOCKED_STATUSES = {"blocked"}
@ -248,7 +249,7 @@ class SSTCService:
[
t
for t in tasks
if t.due_date and t.due_date < datetime.now(timezone.utc).replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES
if t.due_date and t.due_date < now_cn_aware().replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES
]
)
return {
@ -453,7 +454,7 @@ class SSTCService:
overdue_tasks = [
t
for t in tasks
if t.due_date and t.due_date < datetime.now(timezone.utc).replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES
if t.due_date and t.due_date < now_cn_aware().replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES
]
risk_items = [
{

View File

@ -22,6 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import SymbolDefinition, SymbolReference
from ..utils.timezone import CN_TZ, now_cn_aware, now_cn_naive
TS_SYMBOL_RE = re.compile(
r"^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|const)\s+([A-Za-z_][A-Za-z0-9_]*)",
re.MULTILINE,
@ -127,7 +128,7 @@ class SymbolIndexService:
text = file_path.read_text(encoding="utf-8", errors="ignore")
rel_path = self._to_rel_path(file_path=file_path, workspace_root=workspace_root, backend_root=backend_root)
file_hash = hashlib.sha1(text.encode("utf-8")).hexdigest()
mtime = datetime.fromtimestamp(file_path.stat().st_mtime, tz=timezone.utc).replace(tzinfo=None)
mtime = datetime.fromtimestamp(file_path.stat().st_mtime, tz=CN_TZ).replace(tzinfo=None)
if not force_reindex:
latest_hash = await self._get_latest_file_hash(rel_path=rel_path)
if latest_hash and latest_hash == file_hash:
@ -156,7 +157,7 @@ class SymbolIndexService:
await self.db.commit()
stats["errors"] = stats["errors"][:20]
stats["finished_at"] = datetime.now(timezone.utc).isoformat()
stats["finished_at"] = now_cn_aware().isoformat()
return stats
async def search_symbols(
@ -327,7 +328,7 @@ class SymbolIndexService:
{"module": str(path), "symbols": int(cnt or 0)}
for path, cnt in file_groups
],
"snapshot_at": datetime.now(timezone.utc).isoformat(),
"snapshot_at": now_cn_aware().isoformat(),
}
async def _get_latest_file_hash(self, *, rel_path: str) -> Optional[str]:
@ -391,7 +392,7 @@ class SymbolIndexService:
source_mtime=source_mtime,
status="active",
payload=item.payload,
last_indexed_at=datetime.now(timezone.utc).replace(tzinfo=None),
last_indexed_at=now_cn_naive(),
)
self.db.add(row)
created_symbols.append(row)

View File

@ -14,6 +14,7 @@ from ..utils.title import compact_title
from . import workload_service
from .operation_log_service import log_operation
from ..utils.timezone import CN_TZ
TASK_PRIORITY_MAP = {
"p0": "high",
"critical": "high",
@ -98,7 +99,7 @@ def normalize_due_date(value: Any) -> Optional[datetime]:
return None
if isinstance(value, datetime):
if value.tzinfo is not None:
return value.astimezone(timezone.utc).replace(tzinfo=None)
return value.astimezone(CN_TZ).replace(tzinfo=None)
return value
text = str(value).strip()
if not text:

View File

@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import OperationLog, Task, User, UserProject, UserWorkload
from .lark import lark_service
from ..utils.timezone import now_cn_naive
DONE_STATUSES = {"done", "completed", "closed", "resolved", "archived"}
@ -200,7 +201,7 @@ async def process_tenant_unresolved_overloads(
user_rows = rows.all()
created = 0
now = datetime.utcnow()
now = now_cn_naive()
for user_id_raw, ratio_raw in user_rows:
user_id = int(user_id_raw)
ratio = float(ratio_raw or 0.0)

View File

@ -13,6 +13,7 @@ from ..config.ai_prompts import PROMPT_WORKLOAD_OPTIMIZATION
from ..models import AICallLog, Task, User, UserProject, UserSkillBinding, UserWorkload
from .llm import llm_service
from ..utils.timezone import now_cn_naive
LEVEL_HOURS_MULTIPLIER = {
"expert": 0.8,
"advanced": 0.9,
@ -101,7 +102,7 @@ async def recalculate_user_workload_ratio(
)
workload.workload_ratio = ratio
user.workload = int(max(0, min(200, ratio)))
user.updated_at = datetime.utcnow()
user.updated_at = now_cn_naive()
try:
from . import workload_alert_service

View File

@ -11,6 +11,7 @@ import logging
from fastapi import WebSocket, WebSocketDisconnect
from enum import Enum
from .utils.timezone import now_cn_aware, now_cn_naive
logger = logging.getLogger(__name__)
@ -29,7 +30,7 @@ class WebSocketConnection:
self.websocket = websocket
self.client_id = client_id
self.subscriptions: Set[str] = set()
self.connected_at = datetime.utcnow()
self.connected_at = now_cn_naive()
self.last_ping: Optional[datetime] = None
async def send(self, data: Dict[str, Any]) -> bool:
@ -44,8 +45,8 @@ class WebSocketConnection:
async def ping(self) -> bool:
"""发送 ping"""
try:
await self.websocket.send_json({"type": "ping", "timestamp": datetime.utcnow().timestamp()})
self.last_ping = datetime.utcnow()
await self.websocket.send_json({"type": "ping", "timestamp": now_cn_aware().timestamp()})
self.last_ping = now_cn_naive()
return True
except Exception as e:
logger.error(f"Error pinging client {self.client_id}: {e}")
@ -113,7 +114,7 @@ class WebSocketManager:
await connection.send({
"type": ConnectionEventType.CONNECTED,
"client_id": client_id,
"timestamp": datetime.utcnow().timestamp(),
"timestamp": now_cn_aware().timestamp(),
})
logger.info(f"WebSocket client connected: {client_id}")
@ -135,7 +136,7 @@ class WebSocketManager:
await connection.send({
"type": ConnectionEventType.SUBSCRIBED,
"event_types": list(connection.subscriptions),
"timestamp": datetime.utcnow().timestamp(),
"timestamp": now_cn_aware().timestamp(),
})
async def unsubscribe(self, client_id: str, event_types: List[str]):
@ -148,7 +149,7 @@ class WebSocketManager:
await connection.send({
"type": ConnectionEventType.UNSUBSCRIBED,
"event_types": list(connection.subscriptions),
"timestamp": datetime.utcnow().timestamp(),
"timestamp": now_cn_aware().timestamp(),
})
async def broadcast(self, event_type: str, data: Dict[str, Any]) -> int:
@ -166,7 +167,7 @@ class WebSocketManager:
"type": "event",
"event_type": event_type,
"data": data,
"timestamp": datetime.utcnow().timestamp(),
"timestamp": now_cn_aware().timestamp(),
}
success_count = 0

View File

@ -17,6 +17,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from backend.app.utils.timezone import now_cn_aware
try:
import httpx
except Exception: # pragma: no cover
@ -259,7 +260,7 @@ def _build_report(
return {
"version": "v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"generated_at": now_cn_aware().isoformat(),
"mode": mode,
"api_url": api_url,
"thresholds": thresholds,
@ -297,7 +298,7 @@ def _build_report(
def _write_report(report: dict[str, Any], output_dir: Path) -> Path:
output_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
ts = now_cn_aware().strftime("%Y%m%dT%H%M%SZ")
report_path = output_dir / f"ai_steward_eval_{ts}.json"
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
latest = output_dir / "latest.json"

View File

@ -26,6 +26,7 @@ from typing import Any, Iterable
import httpx
from backend.app.utils.timezone import now_cn_aware
DEFAULT_TEMPLATE_LEAK_PATTERNS = [
"当前系统采用前后端分层架构",
"fastapi + sqlalchemy",
@ -823,7 +824,7 @@ async def _amain() -> int:
args.sleep_ms = max(0, min(args.sleep_ms, 10000))
args.timeout_seconds = max(5.0, min(args.timeout_seconds, 300.0))
started_at = datetime.now(timezone.utc).isoformat()
started_at = now_cn_aware().isoformat()
scenarios = _build_scenarios(args.mode)
token = await _login_token(
base_url=args.base_url,
@ -853,7 +854,7 @@ async def _amain() -> int:
]
results = await asyncio.gather(*tasks)
metrics = _merge_metrics(results)
finished_at = datetime.now(timezone.utc).isoformat()
finished_at = now_cn_aware().isoformat()
report = _build_report(
started_at=started_at,