first commit

This commit is contained in:
2026-09-02 11:44:52 +08:00
commit 0c8fa2653e
309 changed files with 57278 additions and 0 deletions
@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""有界分页列出今天或最近几天收到的日志摘要。
脚本只读取列表投影,不再为每条日志调用 ``entry get``。需要正文时,调用方应
从结果中选择明确的 reportId,再单独读取那一条,避免列表任务退化成 N+1。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from datetime import datetime, timedelta
from typing import Any, NamedTuple
from zoneinfo import ZoneInfo
SHANGHAI = ZoneInfo("Asia/Shanghai")
PAGE_SIZE = 20
DEFAULT_MAX_PAGES = 10
HARD_MAX_PAGES = 10
MAX_REPORTS = PAGE_SIZE * HARD_MAX_PAGES
DEFAULT_DISPLAY_LIMIT = 20
PER_COMMAND_TIMEOUT_SECONDS = 60
TOTAL_TIMEOUT_SECONDS = 120
MAX_ERROR_DETAIL_CHARS = 4096
class ReportCommandError(RuntimeError):
"""DWS 执行或响应契约失败,不能降级成合法空结果。"""
class InboxScanResult(NamedTuple):
"""完整扫描证据与受展示上限约束的摘要。"""
total_count: int
visible_items: list[dict[str, Any]]
def query_window(days: int, now: datetime | None = None) -> tuple[datetime, datetime]:
"""冻结查询时间窗,并保证午夜调度也得到严格递增的范围。"""
current = now or datetime.now(SHANGHAI)
start = (current - timedelta(days=days - 1)).replace(
hour=0, minute=0, second=0, microsecond=0
)
end = current.replace(microsecond=0)
if end <= start:
end = start + timedelta(seconds=1)
return start, end
def format_create_time(value: Any) -> str:
"""把服务端 epoch 毫秒转换为带时区的可读时间,未知形态如实保留。"""
if isinstance(value, (int, float)) and not isinstance(value, bool):
return datetime.fromtimestamp(value / 1000, SHANGHAI).strftime(
"%Y-%m-%d %H:%M:%S %z"
)
return str(value or "")
def clip_detail(value: Any, limit: int = MAX_ERROR_DETAIL_CHARS) -> str:
"""把诊断压到固定上限,避免响应正文进入错误日志或模型上下文。"""
if isinstance(value, (dict, list)):
text = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
else:
text = str(value or "")
text = text.strip()
if len(text) <= limit:
return text
return text[: limit - 14] + "…[已截断]"
def process_error_detail(result: subprocess.CompletedProcess[str]) -> str:
"""优先保留结构化错误与 stderr;原始 stdout 仅做有界兜底。"""
parts: list[str] = []
try:
payload = json.loads(result.stdout)
except (json.JSONDecodeError, TypeError):
payload = None
if isinstance(payload, dict):
structured = payload.get("error") or payload.get("message")
if structured:
parts.append("error=" + clip_detail(structured, 2048))
stderr = clip_detail(result.stderr, 2048)
if stderr:
parts.append("stderr=" + stderr)
if not parts:
stdout = clip_detail(result.stdout, 2048)
if stdout:
parts.append("stdout=" + stdout)
return clip_detail("; ".join(parts)) or "无错误详情"
def run_dws(
args: list[str], *, dry_run: bool = False, timeout_seconds: float = 60
) -> Any | None:
cmd = ["dws", *args]
if dry_run:
print("[dry-run] " + " ".join(cmd))
return None
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout_seconds
)
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
raise ReportCommandError(f"DWS 执行失败: {exc}") from exc
if result.returncode != 0:
raise ReportCommandError(
f"DWS 返回非零状态 exit={result.returncode}: "
f"{process_error_detail(result)}"
)
try:
return json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise ReportCommandError(f"DWS 返回的不是合法 JSON: {exc}") from exc
def parse_inbox_page(
payload: Any, current_cursor: int
) -> tuple[list[dict[str, Any]], int | None]:
if not isinstance(payload, dict):
raise ReportCommandError(
f"收件箱响应应为对象,实际为 {type(payload).__name__}"
)
if payload.get("ok") is not True or payload.get("outcome") != "success":
error = payload.get("error")
raise ReportCommandError(
"收件箱调用未成功: " + clip_detail(error or payload)
)
data = payload.get("data")
if not isinstance(data, dict):
raise ReportCommandError("收件箱成功响应缺少 data 对象")
reports = data.get("reports")
if not isinstance(reports, list) or any(
not isinstance(item, dict) for item in reports
):
raise ReportCommandError("收件箱 data.reports 必须是对象数组")
count = data.get("count")
if (
not isinstance(count, int)
or isinstance(count, bool)
or count != len(reports)
):
raise ReportCommandError("收件箱 count 与 reports 数量不一致")
complete = data.get("complete")
if not isinstance(complete, bool):
raise ReportCommandError("收件箱响应缺少布尔 complete")
meta = payload.get("meta")
pagination = meta.get("pagination") if isinstance(meta, dict) else None
if not isinstance(pagination, dict):
raise ReportCommandError("收件箱响应缺少 meta.pagination")
exhausted = pagination.get("endpoint_exhausted")
if not isinstance(exhausted, bool) or exhausted != complete:
raise ReportCommandError("收件箱 data.complete 与分页终止证据冲突")
if exhausted:
return reports, None
raw_next = pagination.get("next_token")
try:
next_cursor = int(raw_next)
except (TypeError, ValueError) as exc:
raise ReportCommandError("收件箱续页缺少整数 next_token") from exc
if next_cursor <= current_cursor:
raise ReportCommandError("收件箱 continuation cursor 没有严格前进")
return reports, next_cursor
def scan_inbox(
start: datetime,
end: datetime,
max_pages: int,
*,
display_limit: int = DEFAULT_DISPLAY_LIMIT,
total_timeout_seconds: float = TOTAL_TIMEOUT_SECONDS,
) -> InboxScanResult:
if not 1 <= display_limit <= MAX_REPORTS:
raise ReportCommandError(
f"展示上限必须在 1..{MAX_REPORTS} 之间"
)
cursor = 0
total_count = 0
visible_items: list[dict[str, Any]] = []
seen: dict[str, Any] = {}
deadline = time.monotonic() + total_timeout_seconds
for _ in range(max_pages):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise ReportCommandError(
f"收件箱分页超过总时限 {total_timeout_seconds:g}"
)
payload = run_dws([
"report", "+inbox-list",
"--start", start.isoformat(timespec="seconds"),
"--end", end.isoformat(timespec="seconds"),
"--cursor", str(cursor),
"--size", str(PAGE_SIZE),
"--format", "json",
], timeout_seconds=max(
0.1, min(PER_COMMAND_TIMEOUT_SECONDS, remaining)
))
page, next_cursor = parse_inbox_page(payload, cursor)
for item in page:
report_id = item.get("reportId")
if not isinstance(report_id, str) or not report_id.strip():
raise ReportCommandError("收件箱条目缺少稳定 reportId")
created = item.get("createTime")
if report_id in seen:
if seen[report_id] != created:
raise ReportCommandError(
f"收件箱重复 reportId 的 createTime 冲突: {report_id}"
)
continue
if total_count >= MAX_REPORTS:
raise ReportCommandError(
f"收件箱结果超过有界条数上限 {MAX_REPORTS}"
)
seen[report_id] = created
total_count += 1
if len(visible_items) < display_limit:
visible_items.append(item)
if next_cursor is None:
return InboxScanResult(total_count, visible_items)
cursor = next_cursor
raise ReportCommandError(
f"达到 --max-pages={max_pages} 时收件箱仍有后续页;"
"拒绝把部分结果伪装成完整列表"
)
def main() -> int:
parser = argparse.ArgumentParser(description="查看收到的日志摘要")
parser.add_argument(
"--days", type=int, default=1, help="查询天数(默认 1"
)
parser.add_argument(
"--max-pages",
type=int,
default=DEFAULT_MAX_PAGES,
help=f"最大分页数(默认 {DEFAULT_MAX_PAGES},范围 1..{HARD_MAX_PAGES}",
)
parser.add_argument(
"--display-limit",
type=int,
default=DEFAULT_DISPLAY_LIMIT,
help=f"最多展开的摘要数(默认 {DEFAULT_DISPLAY_LIMIT},范围 1..{MAX_REPORTS}",
)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
if args.days < 1:
parser.error("--days must be >= 1")
if not 1 <= args.max_pages <= HARD_MAX_PAGES:
parser.error(
f"--max-pages must be between 1 and {HARD_MAX_PAGES}"
)
if not 1 <= args.display_limit <= MAX_REPORTS:
parser.error(
f"--display-limit must be between 1 and {MAX_REPORTS}"
)
# 以调用开始时刻冻结查询窗,避免分页过程中把未来新增条目插进结果集。
start, end = query_window(args.days)
label = "今天" if args.days == 1 else f"最近 {args.days}"
if args.dry_run:
run_dws([
"report", "+inbox-list",
"--start", start.isoformat(timespec="seconds"),
"--end", end.isoformat(timespec="seconds"),
"--cursor", "0",
"--size", str(PAGE_SIZE),
"--format", "json",
], dry_run=True)
return 0
scan = scan_inbox(
start, end, args.max_pages, display_limit=args.display_limit
)
if scan.total_count == 0:
print(f"{label}暂无收到的日志")
return 0
print(f"{label}收到的日志({scan.total_count} 条,已完成分页)")
for item in scan.visible_items:
creator = (
item.get("creatorName")
or item.get("creatorUserId")
or "未知创建人"
)
template = item.get("templateName") or "日志"
print(
f"- {template} | {creator} | "
f"{format_create_time(item.get('createTime'))} | {item['reportId']}"
)
if len(scan.visible_items) < scan.total_count:
print(
f"另有 {scan.total_count - len(scan.visible_items)} 条未展开;"
f"需要时用 --display-limit {min(scan.total_count, MAX_REPORTS)} 显示。"
)
print(
"需要正文时,请选择上面的明确 reportId "
"再执行 dws report entry get。"
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except ReportCommandError as exc:
print(f"错误:{exc}", file=sys.stderr)
raise SystemExit(2) from exc