分析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", "version": "v2",
"updated_at": "2026-03-11T00:00:00Z", "updated_at": "2026-03-11T08:35:00Z",
"source_of_truth": "openclaw", "source_of_truth": "openclaw",
"description": "PMFlow 三引擎统一路由:所有主链路统一经 OpenClaw OpenAI-Compatible 网关。", "description": "PMFlow 三引擎统一路由:所有主链路统一经 OpenClaw OpenAI-Compatible 网关。",
"rules": [ "rules": [
@ -10,7 +10,7 @@
"priority": 320, "priority": 320,
"tier": "high", "tier": "high",
"provider": "openai", "provider": "openai",
"model": "MiniMax-M2.5", "model": "GLM-5",
"api_env": "OPENAI_API_KEY", "api_env": "OPENAI_API_KEY",
"function_exact": "classify_intent_complex" "function_exact": "classify_intent_complex"
}, },
@ -32,12 +32,12 @@
] ]
}, },
{ {
"name": "engine_high_reasoning_minimax", "name": "engine_high_reasoning_glm5",
"enabled": true, "enabled": true,
"priority": 220, "priority": 220,
"tier": "high", "tier": "high",
"provider": "openai", "provider": "openai",
"model": "MiniMax-M2.5", "model": "GLM-5",
"api_env": "OPENAI_API_KEY", "api_env": "OPENAI_API_KEY",
"match_any": [ "match_any": [
"adaptive_rag_answer", "adaptive_rag_answer",
@ -62,7 +62,7 @@
"priority": 200, "priority": 200,
"tier": "high", "tier": "high",
"provider": "openai", "provider": "openai",
"model": "MiniMax-M2.5", "model": "GLM-5",
"api_env": "OPENAI_API_KEY", "api_env": "OPENAI_API_KEY",
"match_any": [ "match_any": [
"analyze_food_calories", "analyze_food_calories",
@ -80,9 +80,9 @@
"enabled": true, "enabled": true,
"priority": 50, "priority": 50,
"tier": "safety", "tier": "safety",
"provider": "openai", "provider": "glm",
"model": "deepseek-chat", "model": "GLM-4.7",
"api_env": "OPENAI_API_KEY", "api_env": "GLM_API_KEY",
"match_all": true "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` 强制进入人工确认,不直接自动执行 - AI PMO 意图仲裁默认开启(`AI_PMO_INTENT_ARBITRATION_ENABLED=true`):规则与 LLM 写操作冲突时按 `AI_PMO_INTENT_CONFLICT_POLICY=pending_manual` 强制进入人工确认,不直接自动执行
- `project.create` 启用槽位完整性闸门:无法从指令提取有效项目名时标记 `slot_incomplete` 并进入人工确认 - `project.create` 启用槽位完整性闸门:无法从指令提取有效项目名时标记 `slot_incomplete` 并进入人工确认
- 数据库与核心写入链路统一使用北京时间(`Asia/Shanghai`):PostgreSQL 会话时区、数据库默认时区与核心 `_utc_now*` 写入入口均已切换到北京时间 - 数据库与核心写入链路统一使用北京时间(`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(): if not _is_postgresql():
return return
_shift_all_timestamp_columns(hours=-8) _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.openclaw_commander_service import OpenClawCommanderService
from ..services.signal_source_orchestration_service import SignalSourceOrchestrationService from ..services.signal_source_orchestration_service import SignalSourceOrchestrationService
from ..schemas.rag import RAGAskRequest 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"]) router = APIRouter(prefix="/ai-steward", tags=["AI Steward"])
@ -264,7 +264,7 @@ async def onboard_signal_source(
{ {
"source": "pmflow_manual_signal_source_onboard", "source": "pmflow_manual_signal_source_onboard",
"instruction": request.instruction[:2000], "instruction": request.instruction[:2000],
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": now_cn_aware().isoformat(),
}, },
ensure_ascii=False, ensure_ascii=False,
), ),
@ -1103,7 +1103,7 @@ async def get_ai_steward_stats(
'active_blockers': active_blockers, 'active_blockers': active_blockers,
'active_projects': active_projects, 'active_projects': active_projects,
'hours_saved': hours_saved, 'hours_saved': hours_saved,
'last_updated': datetime.now(timezone.utc).isoformat() 'last_updated': now_cn_aware().isoformat()
} }
return stats return stats

View File

@ -20,6 +20,7 @@ from ..schemas.budget import (
) )
from ..services.budget_enforcement_service import BudgetEnforcementService from ..services.budget_enforcement_service import BudgetEnforcementService
from ..utils.timezone import now_cn_naive
router = APIRouter(prefix="/budget", tags=["budget"]) router = APIRouter(prefix="/budget", tags=["budget"])
@ -84,7 +85,7 @@ async def create_budget(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
_ = current_user _ = current_user
now = datetime.utcnow() now = now_cn_naive()
period_start, period_end = _period_window(now, request.period) period_start, period_end = _period_window(now, request.period)
new_budget = Budget( new_budget = Budget(
tenant_id=tenant_id, 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 ..models import Notification, User
from ..utils.timezone import now_cn_naive
router = APIRouter(prefix="/notifications", tags=["notifications"]) router = APIRouter(prefix="/notifications", tags=["notifications"])
@ -136,7 +137,7 @@ async def mark_notification_read(
if not row.is_read: if not row.is_read:
row.is_read = True row.is_read = True
row.read_at = datetime.now(timezone.utc) row.read_at = now_cn_naive()
await db.commit() await db.commit()
return success_response(data=_serialize_notification(row)) return success_response(data=_serialize_notification(row))
@ -160,7 +161,7 @@ async def mark_all_notifications_read(
) )
).scalars().all() ).scalars().all()
now = datetime.now(timezone.utc) now = now_cn_naive()
for row in rows: for row in rows:
row.is_read = True row.is_read = True
row.read_at = now 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 import skill_service
from ..services.auth import has_any_role from ..services.auth import has_any_role
from ..utils.timezone import now_cn_naive
router = APIRouter(tags=["skill-tags"]) router = APIRouter(tags=["skill-tags"])
@ -141,7 +142,7 @@ async def update_skill_tag(
tag.description = body.description tag.description = body.description
if body.sort_order is not None: if body.sort_order is not None:
tag.sort_order = body.sort_order tag.sort_order = body.sort_order
tag.updated_at = datetime.utcnow() tag.updated_at = now_cn_naive()
await db.commit() await db.commit()
await db.refresh(tag) await db.refresh(tag)
return _to_tag_payload(tag) return _to_tag_payload(tag)
@ -285,7 +286,7 @@ async def update_user_skill(
row.quality_score = body.quality_score row.quality_score = body.quality_score
if body.efficiency_score is not None: if body.efficiency_score is not None:
row.efficiency_score = body.efficiency_score row.efficiency_score = body.efficiency_score
row.updated_at = datetime.utcnow() row.updated_at = now_cn_naive()
await db.commit() await db.commit()
await AIPMOKernelService.invalidate_world_model_cache_for_user(db=db, tenant_id=tenant_id, user_id=user_id) await AIPMOKernelService.invalidate_world_model_cache_for_user(db=db, tenant_id=tenant_id, user_id=user_id)
return {"message": "Skill updated"} 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.title import compact_title
from ..utils.timezone import CN_TZ, now_cn_aware, now_cn_naive
router = APIRouter(tags=["sstc"]) router = APIRouter(tags=["sstc"])
_FEISHU_PLACEHOLDER_RE = re.compile(r"^飞书用户\(([^)]+)\)$") _FEISHU_PLACEHOLDER_RE = re.compile(r"^飞书用户\(([^)]+)\)$")
_SIGNAL_PREFIX_RE = re.compile(r"^\[(.*?)\]\s*:\s*") _SIGNAL_PREFIX_RE = re.compile(r"^\[(.*?)\]\s*:\s*")
@ -866,7 +867,7 @@ async def _build_ai_first_charter_content(
fallback_model = "同供应商低成本模型" fallback_model = "同供应商低成本模型"
source_refs = [f"signal:{sid}" for sid in signal_ids] 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_m1 = _default_ai_validations("模块1:触发与目标")
validations_m2 = _default_ai_validations("模块2:范围与对齐") validations_m2 = _default_ai_validations("模块2:范围与对齐")
validations_m3 = _default_ai_validations("模块3:模型与依赖") 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()], "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()], "root_causes": [str(x).strip() for x in root_causes if str(x).strip()],
"improvements": normalized_improvements, "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 created_at_dt = s.created_at
if created_at_dt is not None and created_at_dt.tzinfo is not None: 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( if not _match_signal_filters(
row=row, row=row,
status_filter=normalized_status_filter, status_filter=normalized_status_filter,
@ -1911,7 +1912,7 @@ async def upload_signal_attachment(
"image_key": None, "image_key": None,
"media_id": None, "media_id": None,
"source": "manual_upload", "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), "download_url": _build_signal_attachment_download_url(pid, sid, attachment_id),
} }
@ -2918,7 +2919,7 @@ async def get_task_board(
risk_count = 0 risk_count = 0
if (t.status or "").lower() == "blocked": if (t.status or "").lower() == "blocked":
risk_count += 1 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 risk_count += 1
rows.append( rows.append(
{ {
@ -2960,7 +2961,7 @@ async def patch_task_status(
before = {"status": task.status} before = {"status": task.status}
task.status = body.status task.status = body.status
if body.status in DONE_STATUSES: 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( await log_operation(
db, db,
tenant_id=tenant_id, tenant_id=tenant_id,
@ -3420,7 +3421,7 @@ async def patch_release_status(
release_node.status = body.status release_node.status = body.status
content = _as_dict(release_node.content) content = _as_dict(release_node.content)
content["status_note"] = body.note 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 release_node.content = content
await log_operation( await log_operation(
db, db,
@ -3525,7 +3526,7 @@ async def patch_release_verification_item(
target["status"] = body.status target["status"] = body.status
if body.status == "done": if body.status == "done":
target["verified_by"] = str(current_user.id) target["verified_by"] = str(current_user.id)
target["verified_at"] = datetime.now(timezone.utc).isoformat() target["verified_at"] = now_cn_aware().isoformat()
else: else:
target["verified_by"] = None target["verified_by"] = None
target["verified_at"] = 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"] incomplete = [item for item in items if str(item.get("status") or "").lower() != "done"]
if incomplete: if incomplete:
raise HTTPException(status_code=409, detail="仍有未完成验证项,请先逐项完成") 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_items"] = items
content["verification_done"] = True content["verification_done"] = True
content["verification_progress"] = {"done": len(items), "total": len(items)} 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" review_node.status = "archived" if body.archive else "draft"
content = _as_dict(review_node.content) content = _as_dict(review_node.content)
content["archive_note"] = body.note 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 review_node.content = content
await log_operation( await log_operation(
db, 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.operation_log_service import log_operation
from ..services import skill_service from ..services import skill_service
from ..utils.timezone import now_cn_naive
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
COMPLETED_TASK_STATUSES = {"done", "completed", "closed", "resolved", "archived"} 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) user.password_hash = AuthService.hash_password(body.new_password)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from 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( await log_operation(
db, db,
tenant_id=tenant_id, tenant_id=tenant_id,
@ -696,7 +697,7 @@ async def patch_user_status(
Task.assignee_id == user_id, 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( workload_result = await db.execute(
select(UserWorkload).where( select(UserWorkload).where(
@ -986,7 +987,7 @@ async def update_user_workload(
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
user.workload = max(0, min(200, int(workload))) user.workload = max(0, min(200, int(workload)))
user.updated_at = datetime.utcnow() user.updated_at = now_cn_naive()
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
return _serialize_user(user) return _serialize_user(user)

View File

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

View File

@ -12,6 +12,7 @@ import re
import logging import logging
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .utils.timezone import now_cn_aware
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -356,7 +357,7 @@ class RoutingResult(BaseModel):
confidence: float confidence: float
details: Dict[str, Any] details: Dict[str, Any]
signal_id: Optional[str] = None 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 import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from .utils.timezone import now_cn_aware
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -74,7 +75,7 @@ class Event:
type: EventType type: EventType
data: Dict[str, Any] 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 source: Optional[str] = None
correlation_id: Optional[str] = None correlation_id: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict) 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.feishu_integration import dispatch_pending_feishu_replies
from .services.context_cache import register_context_cache_invalidation_hooks from .services.context_cache import register_context_cache_invalidation_hooks
from .utils.timezone import now_cn_aware
configure_logging(settings.debug) configure_logging(settings.debug)
logger = get_logger(__name__) logger = get_logger(__name__)
register_context_cache_invalidation_hooks() register_context_cache_invalidation_hooks()
@ -422,7 +423,7 @@ async def _get_recent_active_projects(
limit_per_tenant: int, limit_per_tenant: int,
max_projects: int, max_projects: int,
) -> list[tuple[int, 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)) per_tenant_limit = max(1, int(limit_per_tenant))
total_limit = max(per_tenant_limit, int(max_projects)) 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 .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: def _env_int(name: str, default: int, *, minimum: int, maximum: int) -> int:
raw = str(os.getenv(name, "") or "").strip() raw = str(os.getenv(name, "") or "").strip()
if not raw: if not raw:
@ -1408,7 +1409,7 @@ class AdaptiveRAGService:
"original_question": str(question or "")[:4000], "original_question": str(question or "")[:4000],
"normalized_question": summary, "normalized_question": summary,
"write_mode": "ack_first", "write_mode": "ack_first",
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": now_cn_aware().isoformat(),
} }
signal = Signal( signal = Signal(
source="PMFlow", source="PMFlow",
@ -2366,7 +2367,7 @@ class AdaptiveRAGService:
history = session_state.rewrite_history if isinstance(session_state.rewrite_history, list) else [] history = session_state.rewrite_history if isinstance(session_state.rewrite_history, list) else []
history.append( history.append(
{ {
"at": datetime.now(timezone.utc).isoformat(), "at": now_cn_aware().isoformat(),
"question": question[:500], "question": question[:500],
"effective_question": effective_question[:500], "effective_question": effective_question[:500],
"query_type": str(query_type or QUERY_TYPE_UNKNOWN), "query_type": str(query_type or QUERY_TYPE_UNKNOWN),
@ -3116,7 +3117,7 @@ class AdaptiveRAGService:
symbol_snapshot=symbol_snapshot, symbol_snapshot=symbol_snapshot,
kg_overview=kg_overview, kg_overview=kg_overview,
), ),
"generated_at": datetime.now(timezone.utc).isoformat(), "generated_at": now_cn_aware().isoformat(),
} }
async def entity_resolution( async def entity_resolution(
@ -3516,7 +3517,7 @@ class AdaptiveRAGService:
"python_symbols": 0, "python_symbols": 0,
"typescript_symbols": 0, "typescript_symbols": 0,
"top_modules": [], "top_modules": [],
"generated_at": datetime.now(timezone.utc).isoformat(), "generated_at": now_cn_aware().isoformat(),
} }
candidates = [ candidates = [
@ -3568,7 +3569,7 @@ class AdaptiveRAGService:
"python_symbols": int(python_symbols), "python_symbols": int(python_symbols),
"typescript_symbols": int(ts_symbols), "typescript_symbols": int(ts_symbols),
"top_modules": top_modules, "top_modules": top_modules,
"generated_at": datetime.now(timezone.utc).isoformat(), "generated_at": now_cn_aware().isoformat(),
} }
@staticmethod @staticmethod
@ -4730,7 +4731,7 @@ class AdaptiveRAGService:
file_url=(file_url or "")[:1024] or None, file_url=(file_url or "")[:1024] or None,
uploaded_by=uploaded_by, uploaded_by=uploaded_by,
status="processing", 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) self.db.add(doc)
await self.db.commit() await self.db.commit()
@ -4814,7 +4815,7 @@ class AdaptiveRAGService:
doc.status = "complete" doc.status = "complete"
doc.error_message = None doc.error_message = None
if source_type == "url": if source_type == "url":
doc.last_fetched_at = datetime.now(timezone.utc) doc.last_fetched_at = now_cn_naive()
await self.db.commit() await self.db.commit()
except Exception as exc: except Exception as exc:
doc.status = "failed" doc.status = "failed"
@ -4906,7 +4907,7 @@ class AdaptiveRAGService:
"sender": sender, "sender": sender,
"chat_id": chat_id, "chat_id": chat_id,
"message_id": message_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, "project_id": int(project_id) if project_id is not None else None,
"target_url": target_url, "target_url": target_url,
"title": title[:255], "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, "risk_level": risk,
"target_url": None, "target_url": None,
"title": title, "title": title,
"updated_at": datetime.now(timezone.utc).isoformat(), "updated_at": now_cn_aware().isoformat(),
} }
return record_id, title, content, metadata return record_id, title, content, metadata
@ -5268,7 +5269,7 @@ class AdaptiveRAGService:
return None return None
if value.tzinfo is None: if value.tzinfo is None:
return value return value
return value.astimezone(timezone.utc).replace(tzinfo=None) return value.astimezone(CN_TZ).replace(tzinfo=None)
@staticmethod @staticmethod
def _normalize_session_title_raw(text: str, *, max_len: int = 30) -> str: def _normalize_session_title_raw(text: str, *, max_len: int = 30) -> str:
@ -5305,7 +5306,7 @@ class AdaptiveRAGService:
if asked_by is None: if asked_by is None:
return (None, 1) return (None, 1)
now_utc = datetime.now(timezone.utc).replace(tzinfo=None) now_utc = now_cn_naive()
chosen: Optional[QASession] = None chosen: Optional[QASession] = None
preferred_id = int(preferred_session_id) if preferred_session_id else 0 preferred_id = int(preferred_session_id) if preferred_session_id else 0
if preferred_id > 0: if preferred_id > 0:

View File

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

View File

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

View File

@ -72,8 +72,8 @@ from .openclaw_commander_service import OpenClawCommanderService
from .ai_pmo_domain_workflow import SignalSourceDomainWorkflow from .ai_pmo_domain_workflow import SignalSourceDomainWorkflow
from .llm import llm_service from .llm import llm_service
from .signal_source_orchestration_service import SignalSourceOrchestrationService 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__) logger = get_logger(__name__)
INTENT_QUERY = "query.read_only" INTENT_QUERY = "query.read_only"
@ -103,6 +103,18 @@ RECEIPT_PHASE_ACTION = "action_applied"
RECEIPT_PHASE_PENDING = "pending_manual" RECEIPT_PHASE_PENDING = "pending_manual"
RECEIPT_PHASE_ERROR = "error" 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 = ( QUESTION_HINTS = (
"?", "?",
"?", "?",
@ -1035,6 +1047,19 @@ class AIPMOKernelService:
project_hint_id: Optional[int] = None, project_hint_id: Optional[int] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
started = time.perf_counter() 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) clean_text = self._normalize_text(message_text)
if not clean_text: if not clean_text:
return {"handled": True, "status": "ignored", "classification": "empty", "direct_reply_required": False} return {"handled": True, "status": "ignored", "classification": "empty", "direct_reply_required": False}
@ -1096,6 +1121,7 @@ class AIPMOKernelService:
source_message_id=synthetic_message_id, source_message_id=synthetic_message_id,
owner_user_id=_safe_int(getattr(operator, "id", None)), 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) session = await self._get_or_create_session(tenant_id=self.tenant_id, sender=sender)
try: try:
@ -1106,6 +1132,7 @@ class AIPMOKernelService:
query_tables=world_model_query_tables, query_tables=world_model_query_tables,
session=session, session=session,
) )
stage_started_at = time.perf_counter()
world_model = await self.build_world_model( world_model = await self.build_world_model(
tenant_id=self.tenant_id, tenant_id=self.tenant_id,
project_id=_safe_int(getattr(project, "id", None)), project_id=_safe_int(getattr(project, "id", None)),
@ -1113,13 +1140,17 @@ class AIPMOKernelService:
project=project, project=project,
pre_queried_tables=world_model_query_tables, pre_queried_tables=world_model_query_tables,
) )
_log_perf("_build_world_model", stage_started_at)
self._schedule_portfolio_snapshot_persist(world_model=world_model) self._schedule_portfolio_snapshot_persist(world_model=world_model)
work_item.state = STATE_MODELED work_item.state = STATE_MODELED
stage_started_at = time.perf_counter()
intent, intent_context_source = await self._resolve_intent( intent, intent_context_source = await self._resolve_intent(
message_text=clean_text, message_text=clean_text,
session=session, session=session,
world_model=world_model, world_model=world_model,
) )
perf_intent_name = intent.name
_log_perf("_resolve_intent", stage_started_at)
if intent_context_source: if intent_context_source:
world_model["context_source"] = intent_context_source world_model["context_source"] = intent_context_source
work_item.context_snapshot = world_model work_item.context_snapshot = world_model
@ -1162,11 +1193,13 @@ class AIPMOKernelService:
work_item.intent = intent.name work_item.intent = intent.name
work_item.project_id = intent.project_id work_item.project_id = intent.project_id
stage_started_at = time.perf_counter()
decision_payload = self.decision_engine.decide( decision_payload = self.decision_engine.decide(
intent=intent, intent=intent,
world_model=world_model, world_model=world_model,
message_text=clean_text, 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" 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")) 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() 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], last_message_text=clean_text[:200],
) )
await self.db.commit() 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, work_item=work_item,
intent=intent, intent=intent,
execution_result=work_item.execution_result or {}, execution_result=work_item.execution_result or {},
@ -1300,11 +1334,14 @@ class AIPMOKernelService:
status="captured", status="captured",
classification="command", classification="command",
) )
_log_perf("_build_capture_result", stage_started_at)
return capture_result
work_item.state = STATE_PLANNED work_item.state = STATE_PLANNED
work_item.action_bundle = decision_payload["execution_plan"] work_item.action_bundle = decision_payload["execution_plan"]
work_item.state = STATE_EXECUTING work_item.state = STATE_EXECUTING
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}: if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
stage_started_at = time.perf_counter()
execution_result, verification = await self._execute_followup_intent( execution_result, verification = await self._execute_followup_intent(
work_item=work_item, work_item=work_item,
intent=intent, intent=intent,
@ -1317,7 +1354,9 @@ class AIPMOKernelService:
source_sender_open_id=source_sender_open_id, source_sender_open_id=source_sender_open_id,
world_model=world_model, world_model=world_model,
) )
_log_perf("TypedExecutor.execute", stage_started_at)
else: else:
stage_started_at = time.perf_counter()
execution_result = await self.executor.execute( execution_result = await self.executor.execute(
intent=intent, intent=intent,
message_text=clean_text, message_text=clean_text,
@ -1326,6 +1365,7 @@ class AIPMOKernelService:
source_system=source_system, source_system=source_system,
source_message_id=synthetic_message_id, 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) verification = await self._verify_execution(intent=intent, execution_result=execution_result, world_model=world_model)
if isinstance(execution_result, dict): if isinstance(execution_result, dict):
execution_result.setdefault("decision_trace", decision_trace) execution_result.setdefault("decision_trace", decision_trace)
@ -1399,7 +1439,8 @@ class AIPMOKernelService:
last_message_text=clean_text[:200], last_message_text=clean_text[:200],
) )
await self.db.commit() 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, work_item=work_item,
intent=intent, intent=intent,
execution_result=execution_result, execution_result=execution_result,
@ -1412,6 +1453,8 @@ class AIPMOKernelService:
else ("signal" if intent.name == INTENT_SIGNAL_CREATE else "command") 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: except Exception as exc:
await self.db.rollback() await self.db.rollback()
logger.warning( logger.warning(
@ -3160,6 +3203,17 @@ class AIPMOKernelService:
classification: str, classification: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
linked_project_ids = [int(work_item.project_id)] if work_item.project_id is not None else [] 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 = { result = {
"handled": True, "handled": True,
"status": status, "status": status,
@ -3179,7 +3233,7 @@ class AIPMOKernelService:
], ],
"intent": intent.name, "intent": intent.name,
"direct_reply_required": bool(direct_reply_required), "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(), "reply_template_key": str(render.get("template_key") or "").strip(),
"work_item_id": int(work_item.id), "work_item_id": int(work_item.id),
"decision_level": work_item.decision_level, "decision_level": work_item.decision_level,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from ..utils.timezone import now_cn_aware
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -91,7 +92,7 @@ class CodeDraftService:
""" """
生成代码草案(激进策略) 生成代码草案(激进策略)
""" """
start_time = datetime.utcnow().timestamp() start_time = now_cn_aware().timestamp()
try: try:
# 1. 读取目标文件 # 1. 读取目标文件
@ -130,7 +131,7 @@ class CodeDraftService:
# 10. 收集警告 # 10. 收集警告
warnings = self._collect_warnings(generated_files, file_contexts) 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( return CodeDraftResponse(
success=True, success=True,
@ -158,7 +159,7 @@ class CodeDraftService:
confidence=0.0, confidence=0.0,
warnings=[f"生成过程出错: {str(e)}"], warnings=[f"生成过程出错: {str(e)}"],
review_points=[], review_points=[],
generation_time=datetime.utcnow().timestamp() - start_time, generation_time=now_cn_aware().timestamp() - start_time,
) )
async def _read_target_files( 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 ..models import Project, ProjectSignalLink, Signal, Task, User, UserProject, UserWorkload
from ..utils.timezone import now_cn_aware
try: try:
import redis.asyncio as redis_async import redis.asyncio as redis_async
from redis import Redis as RedisSync from redis import Redis as RedisSync
@ -178,7 +179,7 @@ class ContextCacheService:
"projects": projects.get("items", []), "projects": projects.get("items", []),
"team_members": team, "team_members": team,
"pending_signals": pending, "pending_signals": pending,
"snapshot_at": datetime.now(timezone.utc).isoformat(), "snapshot_at": now_cn_aware().isoformat(),
} }
@staticmethod @staticmethod
@ -558,7 +559,7 @@ class ContextCacheService:
"backend_root": str(backend_root), "backend_root": str(backend_root),
"app_python_files": app_python_files, "app_python_files": app_python_files,
"feature_catalog_source": str(feature_catalog.get("source") or "unknown"), "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 = { payload = {
"items": items, "items": items,
"count": len(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) await self._set_json(key, payload, PROJECT_LIST_TTL_SECONDS)
return payload return payload
@ -759,7 +760,7 @@ class ContextCacheService:
"p0_count": int(p0_count or 0), "p0_count": int(p0_count or 0),
"p1_count": int(p1_count or 0), "p1_count": int(p1_count or 0),
"p2_count": int(p2_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) await self._set_json(key, payload, SIGNAL_PENDING_TTL_SECONDS)
return payload return payload
@ -892,7 +893,7 @@ class ContextCacheService:
], ],
"pending_signals": pending_signals, "pending_signals": pending_signals,
"workload_ratio": workload_ratio, "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]]: 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 .llm import LLMService
from ..utils.timezone import now_cn_aware
def _parse_involved_user_ids(raw_value: Any) -> List[int]: def _parse_involved_user_ids(raw_value: Any) -> List[int]:
"""兼容 involved_user_ids 的 list / json-string / 单值字符串。""" """兼容 involved_user_ids 的 list / json-string / 单值字符串。"""
if raw_value is None: if raw_value is None:
@ -86,7 +87,7 @@ class DailyBriefingService:
raise ValueError(f"User {user_id} not found") raise ValueError(f"User {user_id} not found")
# 计算时间范围 # 计算时间范围
now = datetime.now(timezone.utc) now = now_cn_aware()
yesterday = now - timedelta(days=1) yesterday = now - timedelta(days=1)
# 根据角色收集相关信息 # 根据角色收集相关信息
@ -456,7 +457,7 @@ class DailyBriefingService:
return { return {
'project_id': project_id, 'project_id': project_id,
'project_name': project.title, 'project_name': project.title,
'generated_at': datetime.now(timezone.utc).isoformat(), 'generated_at': now_cn_aware().isoformat(),
'task_summary': { 'task_summary': {
'total': len(tasks), 'total': len(tasks),
'by_status': status_counts, 'by_status': status_counts,

View File

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

View File

@ -30,6 +30,7 @@ from .llm import LLMService
from ..events import EventType, Event, get_event_bus from ..events import EventType, Event, get_event_bus
from ..utils.timezone import now_cn_aware
class WorkflowEventType: class WorkflowEventType:
"""工作流事件类型""" """工作流事件类型"""
HIGH_IMPACT_SIGNAL = "high_impact_signal" HIGH_IMPACT_SIGNAL = "high_impact_signal"
@ -92,7 +93,7 @@ class EventDrivenWorkflowEngine:
{ {
'workflow_type': event_type, 'workflow_type': event_type,
'result': result, '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 ..models import Experiment, ExperimentTruthNodeSync, TruthNode
from ..utils.timezone import now_cn_aware, now_cn_naive
class ExperimentSyncService: class ExperimentSyncService:
"""实验结果 -> 宪章同步,支持自动应用、人工审核与回滚。""" """实验结果 -> 宪章同步,支持自动应用、人工审核与回滚。"""
@ -147,7 +148,7 @@ class ExperimentSyncService:
return {"sync_id": record.id, "apply_status": record.apply_status} return {"sync_id": record.id, "apply_status": record.apply_status}
record.reviewed_by = reviewer_id record.reviewed_by = reviewer_id
record.reviewed_at = datetime.now(timezone.utc) record.reviewed_at = now_cn_naive()
record.review_notes = review_notes record.review_notes = review_notes
if action == "approve": if action == "approve":
@ -192,7 +193,7 @@ class ExperimentSyncService:
record.apply_status = "requires_review" record.apply_status = "requires_review"
record.reviewed_by = reviewer_id 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" record.review_notes = rollback_notes or "manual rollback"
await self.db.commit() await self.db.commit()
@ -293,7 +294,7 @@ class ExperimentSyncService:
"sync_type": record.sync_type, "sync_type": record.sync_type,
"confidence_score": record.confidence_score, "confidence_score": record.confidence_score,
"experiment_summary": (record.sync_data or {}).get("experiment_summary", {}), "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 content["experiment_sync"] = sync_section
truth_node.content = content truth_node.content = content

View File

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

View File

@ -18,6 +18,7 @@ from ..core.logging import get_logger
from ..models import Integration from ..models import Integration
from .feishu_integration import FeishuIntegrationService from .feishu_integration import FeishuIntegrationService
from ..utils.timezone import CN_TZ, now_cn_aware
logger = get_logger(__name__) logger = get_logger(__name__)
DEFAULT_FEISHU_LONG_CONNECTION_RETRY_MAX_ATTEMPTS = 3 DEFAULT_FEISHU_LONG_CONNECTION_RETRY_MAX_ATTEMPTS = 3
DEFAULT_FEISHU_LONG_CONNECTION_RETRY_BASE_SECONDS = 1.0 DEFAULT_FEISHU_LONG_CONNECTION_RETRY_BASE_SECONDS = 1.0
@ -144,7 +145,7 @@ class FeishuLongConnectionManager:
"last_disconnect_at": self._last_disconnect_at, "last_disconnect_at": self._last_disconnect_at,
"restart_attempts": self._restart_attempts, "restart_attempts": self._restart_attempts,
"next_retry_at": ( "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 if self._next_retry_at > 0
else None else None
), ),
@ -249,7 +250,7 @@ class FeishuLongConnectionManager:
try: try:
await self._preflight_auth(tenant_id=self._tenant_id) 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: except Exception as exc:
code, retryable = self._classify_error(exc) code, retryable = self._classify_error(exc)
self._set_error(str(exc), code=code, retryable=retryable) self._set_error(str(exc), code=code, retryable=retryable)
@ -266,7 +267,7 @@ class FeishuLongConnectionManager:
return False return False
self._clear_error() 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._last_disconnect_at = None
self._restart_attempts = 0 self._restart_attempts = 0
self._next_retry_at = 0.0 self._next_retry_at = 0.0
@ -403,7 +404,7 @@ class FeishuLongConnectionManager:
logger.warning(f"feishu_long_connection_client_cleanup_failed: {exc}") logger.warning(f"feishu_long_connection_client_cleanup_failed: {exc}")
self._client = None self._client = None
if self._running and not self._stopping: 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: if not self._last_error_code:
self._set_error("feishu long connection thread exited", code="thread_exited", retryable=True) self._set_error("feishu long connection thread exited", code="thread_exited", retryable=True)
self._schedule_retry() self._schedule_retry()
@ -416,6 +417,7 @@ class FeishuLongConnectionManager:
return return
try: try:
perf_t0 = time.perf_counter()
payload = self._to_dict(data) payload = self._to_dict(data)
message_id = "" message_id = ""
event_type = "" event_type = ""
@ -432,6 +434,13 @@ class FeishuLongConnectionManager:
).strip() ).strip()
except Exception: except Exception:
pass 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( logger.info(
"feishu_long_connection_event_received: " "feishu_long_connection_event_received: "
f"event_type={event_type or 'unknown'}, message_id={message_id or 'unknown'}" f"event_type={event_type or 'unknown'}, message_id={message_id or 'unknown'}"
@ -468,7 +477,7 @@ class FeishuLongConnectionManager:
try: try:
await self._process_payload(payload) 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: except Exception as exc:
self._set_error(str(exc), code="event_process_failed", retryable=True) self._set_error(str(exc), code="event_process_failed", retryable=True)
logger.warning(f"feishu_long_connection_process_failed: {exc}") logger.warning(f"feishu_long_connection_process_failed: {exc}")
@ -506,6 +515,10 @@ class FeishuLongConnectionManager:
if event_type: if event_type:
event_data = {**event_data, "type": 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: if event_data:
event_data = {**event_data, "_ingress_source": "long_connection"} 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 .openclaw_commander_service import OpenClawCommanderService, OperationExecutorService
from ..utils.project_members import ensure_project_default_members from ..utils.project_members import ensure_project_default_members
from ..utils.sequence import ensure_project_id_sequence 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__) logger = get_logger(__name__)
PROJECT_PRIORITY_ORDER = { PROJECT_PRIORITY_ORDER = {
@ -4048,7 +4048,7 @@ class FeishuSignalPipelineService:
pending_rows = self._dedupe_queue_items(pending_rows) 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)] 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( today_start = _utc_now().astimezone(CN_TZ).replace(
hour=0, hour=0,
minute=0, minute=0,

View File

@ -16,6 +16,7 @@ from datetime import datetime, timedelta
from enum import Enum from enum import Enum
import httpx import httpx
from ..utils.timezone import now_cn_naive
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -118,12 +119,12 @@ class IntegrationService:
IntegrationType.PLANE: IntegrationHealth( IntegrationType.PLANE: IntegrationHealth(
type=IntegrationType.PLANE, type=IntegrationType.PLANE,
status=IntegrationStatus.PENDING, status=IntegrationStatus.PENDING,
last_check=datetime.utcnow(), last_check=now_cn_naive(),
), ),
IntegrationType.LARK: IntegrationHealth( IntegrationType.LARK: IntegrationHealth(
type=IntegrationType.LARK, type=IntegrationType.LARK,
status=IntegrationStatus.PENDING, 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}"], errors=[f"Plane integration is {health.status.value}"],
) )
start_time = datetime.utcnow() start_time = now_cn_naive()
try: try:
# 获取 Plane 服务 # 获取 Plane 服务
@ -198,7 +199,7 @@ class IntegrationService:
await self._record_failure(integration_type, ", ".join(result.get("errors", []))) 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 health.latency_ms = latency
return TaskSyncResult( return TaskSyncResult(
@ -245,7 +246,7 @@ class IntegrationService:
error=f"Lark integration is {health.status.value}", error=f"Lark integration is {health.status.value}",
) )
start_time = datetime.utcnow() start_time = now_cn_naive()
try: try:
# 获取飞书服务 # 获取飞书服务
@ -259,7 +260,7 @@ class IntegrationService:
await self._record_success(integration_type) 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 health.latency_ms = latency
# 发送事件 # 发送事件
@ -384,12 +385,12 @@ class IntegrationService:
"""检查 Plane 集成健康""" """检查 Plane 集成健康"""
integration_type = IntegrationType.PLANE integration_type = IntegrationType.PLANE
health = self._health_status[integration_type] 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: if not self.config.plane.enabled or not self.config.plane.api_key:
health.status = IntegrationStatus.INACTIVE health.status = IntegrationStatus.INACTIVE
health.message = "Plane integration is disabled" health.message = "Plane integration is disabled"
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
return health return health
try: try:
@ -399,14 +400,14 @@ class IntegrationService:
health.status = IntegrationStatus.ACTIVE health.status = IntegrationStatus.ACTIVE
health.message = "Connected" health.message = "Connected"
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
health.latency_ms = (datetime.utcnow() - start_time).total_seconds() * 1000 health.latency_ms = (now_cn_naive() - start_time).total_seconds() * 1000
except Exception as e: except Exception as e:
logger.warning(f"[Integration] Plane health check failed: {e}") logger.warning(f"[Integration] Plane health check failed: {e}")
health.status = IntegrationStatus.ERROR health.status = IntegrationStatus.ERROR
health.message = f"Health check failed: {str(e)}" health.message = f"Health check failed: {str(e)}"
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
return health return health
@ -414,12 +415,12 @@ class IntegrationService:
"""检查飞书集成健康""" """检查飞书集成健康"""
integration_type = IntegrationType.LARK integration_type = IntegrationType.LARK
health = self._health_status[integration_type] 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: if not self.config.lark.enabled or not self.config.lark.app_id:
health.status = IntegrationStatus.INACTIVE health.status = IntegrationStatus.INACTIVE
health.message = "Lark integration is disabled" health.message = "Lark integration is disabled"
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
return health return health
try: try:
@ -434,14 +435,14 @@ class IntegrationService:
health.status = IntegrationStatus.DEGRADED health.status = IntegrationStatus.DEGRADED
health.message = "Running in simulation mode" health.message = "Running in simulation mode"
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
health.latency_ms = (datetime.utcnow() - start_time).total_seconds() * 1000 health.latency_ms = (now_cn_naive() - start_time).total_seconds() * 1000
except Exception as e: except Exception as e:
logger.warning(f"[Integration] Lark health check failed: {e}") logger.warning(f"[Integration] Lark health check failed: {e}")
health.status = IntegrationStatus.ERROR health.status = IntegrationStatus.ERROR
health.message = f"Health check failed: {str(e)}" health.message = f"Health check failed: {str(e)}"
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
return health return health
@ -494,7 +495,7 @@ class IntegrationService:
async def _record_success(self, integration_type: IntegrationType) -> None: async def _record_success(self, integration_type: IntegrationType) -> None:
"""记录成功操作""" """记录成功操作"""
health = self._health_status[integration_type] health = self._health_status[integration_type]
health.last_success = datetime.utcnow() health.last_success = now_cn_naive()
health.consecutive_failures = 0 health.consecutive_failures = 0
if health.status == IntegrationStatus.ERROR: if health.status == IntegrationStatus.ERROR:
health.status = IntegrationStatus.ACTIVE health.status = IntegrationStatus.ACTIVE
@ -505,7 +506,7 @@ class IntegrationService:
health.last_error = error health.last_error = error
health.error_count += 1 health.error_count += 1
health.consecutive_failures += 1 health.consecutive_failures += 1
health.last_check = datetime.utcnow() health.last_check = now_cn_naive()
# 达到降级阈值 # 达到降级阈值
if health.consecutive_failures >= self.MAX_CONSECUTIVE_FAILURES: 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 ..database import AsyncSessionLocal
from ..models import AICallLog, Integration from ..models import AICallLog, Integration
from ..utils.timezone import now_cn_aware
try: try:
import redis.asyncio as redis_async 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") 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: def _runtime_log(scope: str, message: str) -> None:
print(f"[{_cn_now_text()}][{scope}] {message}") 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 @dataclass
class ChatCompletionResult: class ChatCompletionResult:
text: str text: str
@ -460,7 +479,7 @@ class LLMService:
raise ValueError("api_key 不能为空") raise ValueError("api_key 不能为空")
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) 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()) engine_id = str(uuid4())
defaults = PROVIDER_DEFAULTS[provider] defaults = PROVIDER_DEFAULTS[provider]
engine = { engine = {
@ -521,7 +540,7 @@ class LLMService:
if base_url is not None: if base_url is not None:
target["base_url"] = (base_url or "").strip().rstrip("/") or defaults["base_url"] 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)) integration.config = json.loads(json.dumps(bundle))
await db.commit() await db.commit()
return self._sanitize_engine_for_response(target) return self._sanitize_engine_for_response(target)
@ -1101,6 +1120,21 @@ class LLMService:
if cleaned_system_prompt: if cleaned_system_prompt:
messages.append({"role": "system", "content": cleaned_system_prompt}) messages.append({"role": "system", "content": cleaned_system_prompt})
messages.append({"role": "user", "content": 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: try:
if provider_name == "ollama": if provider_name == "ollama":
@ -1150,6 +1184,25 @@ class LLMService:
} }
async with httpx.AsyncClient(timeout=request_timeout, trust_env=False) as client: async with httpx.AsyncClient(timeout=request_timeout, trust_env=False) as client:
resp = await client.post(endpoint, headers=headers, json=payload) resp = await client.post(endpoint, headers=headers, json=payload)
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() resp.raise_for_status()
data = resp.json() data = resp.json()
choices = data.get("choices") or [] choices = data.get("choices") or []
@ -1174,10 +1227,72 @@ class LLMService:
"LLM 返回空内容: " "LLM 返回空内容: "
f"message_keys={msg_keys}, choice_keys={choice_keys}, choice_preview={choice_preview}" 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: if use_provider_circuit_breaker:
self._record_provider_success(circuit_key) self._record_provider_success(circuit_key)
return ChatCompletionResult(text=text, raw_response=data) return ChatCompletionResult(text=text, raw_response=data)
except Exception as exc: 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: if use_provider_circuit_breaker:
self._record_provider_failure(circuit_key, exc) self._record_provider_failure(circuit_key, exc)
raise raise

View File

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

View File

@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import Notification, User from ..models import Notification, User
from ..utils.timezone import CN_TZ, now_cn_aware
class NotificationService: class NotificationService:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db self.db = db
@ -215,9 +216,9 @@ class NotificationService:
@staticmethod @staticmethod
def _format_cn_time(dt: Optional[datetime]) -> str: def _format_cn_time(dt: Optional[datetime]) -> str:
if not dt: if not dt:
dt = datetime.now(timezone.utc) dt = now_cn_aware()
if dt.tzinfo is None: if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc) dt = dt.replace(tzinfo=CN_TZ)
cn_tz = timezone(timedelta(hours=8)) cn_tz = timezone(timedelta(hours=8))
return dt.astimezone(cn_tz).strftime("%Y-%m-%d %H:%M:%S") 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 .task_persistence import build_task_create_draft, persist_task
from ..utils.project_members import ensure_project_default_members from ..utils.project_members import ensure_project_default_members
from ..utils.sequence import ensure_project_id_sequence from ..utils.sequence import ensure_project_id_sequence
from ..utils.timezone import now_cn_naive
from ..utils.title import compact_title from ..utils.title import compact_title
from ..utils.timezone import now_cn_naive
SUPPORTED_COMMANDER_OPERATION_CODES = ( SUPPORTED_COMMANDER_OPERATION_CODES = (
"project.create", "project.create",
"project.update", "project.update",
@ -2025,7 +2025,7 @@ class OperationExecutorService:
task.status = status task.status = status
if status in DONE_TASK_STATUSES: 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( await log_operation(
self.db, self.db,

View File

@ -32,11 +32,11 @@ from ..models import (
Task, Task,
User, User,
) )
from ..utils.timezone import now_cn_naive
from .context_cache import context_cache_service from .context_cache import context_cache_service
from .symbol_index_service import SymbolIndexService from .symbol_index_service import SymbolIndexService
from ..utils.timezone import now_cn_naive
def _utc_now_naive() -> datetime: def _utc_now_naive() -> datetime:
return now_cn_naive() 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 ..config.ai_prompts import build_default_role_prompt_bundle
from ..models import Integration from ..models import Integration
from ..utils.timezone import now_cn_aware
INTEGRATION_TYPE = "ai_prompt_profiles" INTEGRATION_TYPE = "ai_prompt_profiles"
ROLE_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{1,31}$") ROLE_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{1,31}$")
MAX_ROLES = 12 MAX_ROLES = 12
@ -35,7 +36,7 @@ def _safe_json(value: Any) -> dict[str, Any]:
def _now_iso() -> str: def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat() return now_cn_aware().isoformat()
class PromptProfileService: 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.sequence import ensure_project_id_sequence, ensure_signal_id_sequence
from ..utils.timezone import CN_TZ, now_cn_aware
EMOJI_UNICODE_RE = re.compile( EMOJI_UNICODE_RE = re.compile(
r"[\U0001F300-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF" r"[\U0001F300-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF"
r"\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FAFF" r"\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FAFF"
@ -196,9 +197,9 @@ class ProactiveSignalCaptureService:
- 分享技术文章 - 分享技术文章
""" """
trace_id = f"feishu-{uuid4().hex[:12]}" 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: 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_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 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, processed=True,
strategic_analysis=json.dumps({ strategic_analysis=json.dumps({
'url': url, 'url': url,
'captured_at': datetime.now(timezone.utc).isoformat(), 'captured_at': now_cn_aware().isoformat(),
'user_id': user_id, 'user_id': user_id,
'ai_analysis': analysis.get('analysis', {}) 'ai_analysis': analysis.get('analysis', {})
}), }),
@ -862,7 +863,7 @@ class ProactiveSignalCaptureService:
'repo_url': repo_url, 'repo_url': repo_url,
'event_type': event_type, 'event_type': event_type,
'event_data': event_data, 'event_data': event_data,
'captured_at': datetime.now(timezone.utc).isoformat(), 'captured_at': now_cn_aware().isoformat(),
'ai_analysis': analysis.get('analysis', {}) 'ai_analysis': analysis.get('analysis', {})
}), }),
tasks_generated=json.dumps([]), tasks_generated=json.dumps([]),
@ -922,7 +923,7 @@ class ProactiveSignalCaptureService:
strategic_analysis=json.dumps({ strategic_analysis=json.dumps({
'hn_url': story.get('url'), 'hn_url': story.get('url'),
'hn_points': story.get('points'), 'hn_points': story.get('points'),
'captured_at': datetime.now(timezone.utc).isoformat(), 'captured_at': now_cn_aware().isoformat(),
'ai_analysis': analysis.get('analysis', {}) 'ai_analysis': analysis.get('analysis', {})
}), }),
tasks_generated=json.dumps(analysis.get('suggested_tasks', [])), tasks_generated=json.dumps(analysis.get('suggested_tasks', [])),
@ -970,7 +971,7 @@ class ProactiveSignalCaptureService:
logger.info("signal_capture_ai_think", stage=stage, **safe_payload) logger.info("signal_capture_ai_think", stage=stage, **safe_payload)
try: try:
stage_zh = AI_THINK_STAGE_ZH.get(stage, stage) 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( print(
f"[{now_text}][AI思考][SignalCapture] {stage_zh} " f"[{now_text}][AI思考][SignalCapture] {stage_zh} "
+ json.dumps({"stage": stage, **safe_payload}, ensure_ascii=False) + json.dumps({"stage": stage, **safe_payload}, ensure_ascii=False)
@ -1851,7 +1852,7 @@ class ProactiveSignalCaptureService:
def _format_cn_time(dt: datetime) -> str: def _format_cn_time(dt: datetime) -> str:
safe_dt = dt safe_dt = dt
if safe_dt.tzinfo is None: 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)) cn_tz = timezone(timedelta(hours=8))
return safe_dt.astimezone(cn_tz).strftime("%Y-%m-%d %H:%M:%S") return safe_dt.astimezone(cn_tz).strftime("%Y-%m-%d %H:%M:%S")
@ -2135,9 +2136,9 @@ URL: {url}
requested_at: Optional[datetime] = None, requested_at: Optional[datetime] = None,
) -> Optional[int]: ) -> Optional[int]:
"""当分类为 new_project 时,自动创建项目并关联当前信号。""" """当分类为 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: 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( project_brief = await self._draft_new_project_brief(
message=message, message=message,
tenant_id=tenant_id, tenant_id=tenant_id,

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ from ..models import (
) )
from ..utils.project_members import ensure_project_default_members from ..utils.project_members import ensure_project_default_members
from ..utils.timezone import now_cn_aware
DONE_STATUSES = {"done", "completed"} DONE_STATUSES = {"done", "completed"}
ACTIVE_STATUSES = {"in_progress", "review"} ACTIVE_STATUSES = {"in_progress", "review"}
BLOCKED_STATUSES = {"blocked"} BLOCKED_STATUSES = {"blocked"}
@ -248,7 +249,7 @@ class SSTCService:
[ [
t t
for t in tasks 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 { return {
@ -453,7 +454,7 @@ class SSTCService:
overdue_tasks = [ overdue_tasks = [
t t
for t in tasks 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 = [ risk_items = [
{ {

View File

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

View File

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

View File

@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models import OperationLog, Task, User, UserProject, UserWorkload from ..models import OperationLog, Task, User, UserProject, UserWorkload
from .lark import lark_service from .lark import lark_service
from ..utils.timezone import now_cn_naive
DONE_STATUSES = {"done", "completed", "closed", "resolved", "archived"} DONE_STATUSES = {"done", "completed", "closed", "resolved", "archived"}
@ -200,7 +201,7 @@ async def process_tenant_unresolved_overloads(
user_rows = rows.all() user_rows = rows.all()
created = 0 created = 0
now = datetime.utcnow() now = now_cn_naive()
for user_id_raw, ratio_raw in user_rows: for user_id_raw, ratio_raw in user_rows:
user_id = int(user_id_raw) user_id = int(user_id_raw)
ratio = float(ratio_raw or 0.0) 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 ..models import AICallLog, Task, User, UserProject, UserSkillBinding, UserWorkload
from .llm import llm_service from .llm import llm_service
from ..utils.timezone import now_cn_naive
LEVEL_HOURS_MULTIPLIER = { LEVEL_HOURS_MULTIPLIER = {
"expert": 0.8, "expert": 0.8,
"advanced": 0.9, "advanced": 0.9,
@ -101,7 +102,7 @@ async def recalculate_user_workload_ratio(
) )
workload.workload_ratio = ratio workload.workload_ratio = ratio
user.workload = int(max(0, min(200, ratio))) user.workload = int(max(0, min(200, ratio)))
user.updated_at = datetime.utcnow() user.updated_at = now_cn_naive()
try: try:
from . import workload_alert_service from . import workload_alert_service

View File

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

View File

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

View File

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