chore: initial commit of ai site platform

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 10:31:17 +08:00
commit 4ca82fb58a
203 changed files with 45745 additions and 0 deletions

173
ai-service/generate_log.py Normal file
View File

@@ -0,0 +1,173 @@
"""蓝图生成过程日志:控制台 + 落盘,便于对账。
环境变量:
- GENERATE_LOG_DIR日志目录默认仓库 .runtime/logs/generate
- GENERATE_LOG_LEVELDEBUG/INFO/WARNING默认 INFO
"""
from __future__ import annotations
import logging
import os
import threading
import uuid
from contextvars import ContextVar
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
_root = Path(__file__).resolve().parents[1]
_LOGGER_NAME = "aijianzhan.generate"
_setup_lock = threading.Lock()
_configured = False
_current: ContextVar["GenerateTrace | None"] = ContextVar("generate_trace", default=None)
def _log_dir() -> Path:
raw = (os.getenv("GENERATE_LOG_DIR") or "").strip()
if raw:
return Path(raw).expanduser().resolve()
return (_root / ".runtime" / "logs" / "generate").resolve()
def _level() -> int:
name = (os.getenv("GENERATE_LOG_LEVEL") or "INFO").strip().upper()
return getattr(logging, name, logging.INFO)
def ensure_logging() -> logging.Logger:
"""幂等配置stdout + 按日滚动的 generate.log。"""
global _configured
logger = logging.getLogger(_LOGGER_NAME)
if _configured:
return logger
with _setup_lock:
if _configured:
return logger
logger.setLevel(_level())
logger.propagate = False
fmt = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
sh = logging.StreamHandler()
sh.setLevel(_level())
sh.setFormatter(fmt)
logger.addHandler(sh)
try:
d = _log_dir()
d.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(d / "generate.log", encoding="utf-8")
fh.setLevel(_level())
fh.setFormatter(fmt)
logger.addHandler(fh)
except OSError as e:
logger.warning("无法写入生成日志目录: %s", e)
_configured = True
return logger
class GenerateTrace:
"""单次 /apps/generate 请求的对账轨迹。"""
def __init__(self, run_id: str | None = None) -> None:
self.run_id = run_id or uuid.uuid4().hex[:10]
self.started_at = datetime.now(timezone.utc).isoformat()
self.lines: list[dict[str, Any]] = []
self._token = None
self._logger = ensure_logging()
self._run_file: Path | None = None
try:
d = _log_dir()
d.mkdir(parents=True, exist_ok=True)
self._run_file = d / f"run_{self.run_id}.log"
except OSError:
self._run_file = None
def __enter__(self) -> "GenerateTrace":
self._token = _current.set(self)
self.info("generate.start", run_id=self.run_id)
return self
def __exit__(self, exc_type, exc, tb) -> None:
if exc_type is not None:
self.error(f"generate.failed: {exc}", error=str(exc))
else:
self.info("generate.done")
if self._token is not None:
_current.reset(self._token)
return False
def _emit(self, level: str, message: str, **fields: Any) -> None:
ts = datetime.now(timezone.utc).isoformat()
entry = {"ts": ts, "level": level, "message": message}
if fields:
# 仅保留可 JSON 序列化的简单字段
clean: dict[str, Any] = {}
for k, v in fields.items():
if v is None:
continue
if isinstance(v, (str, int, float, bool)):
clean[k] = v
elif isinstance(v, (list, tuple)) and all(
isinstance(x, (str, int, float, bool)) for x in v
):
clean[k] = list(v)
else:
clean[k] = str(v)[:500]
entry.update(clean)
self.lines.append(entry)
extra = " ".join(f"{k}={v}" for k, v in entry.items() if k not in {"ts", "level", "message"})
line = f"[{self.run_id}] {message}" + (f" | {extra}" if extra else "")
log_fn = getattr(self._logger, level if level in {"debug", "info", "warning", "error"} else "info")
log_fn(line)
if self._run_file is not None:
try:
with self._run_file.open("a", encoding="utf-8") as f:
f.write(f"{ts} [{level.upper()}] {line}\n")
except OSError:
pass
def debug(self, message: str, **fields: Any) -> None:
self._emit("debug", message, **fields)
def info(self, message: str, **fields: Any) -> None:
self._emit("info", message, **fields)
def warning(self, message: str, **fields: Any) -> None:
self._emit("warning", message, **fields)
def error(self, message: str, **fields: Any) -> None:
self._emit("error", message, **fields)
def stage(self, name: str, detail: str = "", **fields: Any) -> None:
msg = f"stage.{name}" + (f": {detail}" if detail else "")
self.info(msg, stage=name, **fields)
def note(self, text: str) -> None:
"""把 warnings/notes 同步进日志。"""
t = (text or "").strip()
if t:
self.info(t)
def notes(self, items: list[str] | None) -> None:
for n in items or []:
self.note(str(n))
def as_payload(self) -> dict[str, Any]:
return {
"run_id": self.run_id,
"started_at": self.started_at,
"log_file": str(self._run_file) if self._run_file else "",
"lines": list(self.lines),
}
def get_trace() -> GenerateTrace | None:
return _current.get()
def glog() -> GenerateTrace | None:
"""当前请求轨迹;无上下文时返回 None调用方需判空"""
return get_trace()