zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

131 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""接受前置检查的实时状态重读(生产模式)。
check_writer_acceptance 是无副作用纯函数,不碰库;本模块负责在**接受时刻**从 muse-example
重读冻结行、Canonical 正文 revision、已确认细纲状态与授权快照,组装严格 live_state。
编排层不得用内存里的旧快照冒充实时状态——这里读到什么,preflight 就拿什么比对。
"""
from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from typing import Any, Mapping
from muse_db import connect
PRODUCTION_POLICY = "writer-production-v1"
# 候选接受窗口:生成后 24 小时内必须完成接受,超期 preflight 报 CANDIDATE_EXPIRED。
ACCEPTANCE_WINDOW = timedelta(hours=24)
class LiveStateError(RuntimeError):
"""实时状态不可读或形状非法——失败关闭,不得带病进入 preflight。"""
def _bare_sha(value: str) -> str:
value = str(value or "")
return value[len("sha256:"):] if value.startswith("sha256:") else value
def _parse_tz(value: Any) -> datetime | None:
"""解析带时区 ISO 时间;非法返回 None(由调用方决定是否失败关闭)。"""
if not isinstance(value, str) or not value:
return None
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed
def _iso_z(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def build_live_acceptance_state(
context: Mapping[str, Any], *, now: datetime | None = None
) -> dict[str, Any]:
"""按 check_writer_acceptance 的 live_state 闭集字段重读实时状态。"""
if not isinstance(context, Mapping):
raise LiveStateError("context 必须是对象")
context_sha = _bare_sha(context.get("contextSnapshot", {}).get("contextSha256"))
if len(context_sha) != 64:
raise LiveStateError("context 缺 contextSnapshot.contextSha256")
work_id = context.get("workId")
target_chapter = context.get("targetChapter")
if not isinstance(work_id, int) or not isinstance(target_chapter, int):
raise LiveStateError("context 缺 workId/targetChapter")
checked_at = now or datetime.now(timezone.utc)
with connect(readonly=True) as conn:
freeze = conn.execute(
"SELECT manifest_sha256, context_sha256, authorization_snapshot, create_time "
"FROM example_context_freeze WHERE context_sha256=%s ORDER BY id DESC LIMIT 1",
(context_sha,),
).fetchone()
if not freeze:
raise LiveStateError("CONTEXT_NOT_FROZEN:冻结上下文未落库,接受前置检查拒绝继续")
manifest_sha, freeze_context_sha, auth_payload, freeze_time = freeze
revision_row = conn.execute(
"SELECT COALESCE(MAX(b.revision),0) FROM muse_content_block b "
"JOIN muse_content_chapter c ON b.chapter_id=c.id AND c.deleted=false "
"WHERE c.work_id=%s AND c.order_no=%s AND b.deleted=false",
(work_id, target_chapter),
).fetchone()
canonical_revision = int(revision_row[0])
outline_row = conn.execute(
"SELECT state FROM example_planning_section WHERE work_id=%s "
"AND section_type='fine_outline' AND target_chapter=%s AND deleted=false "
"ORDER BY version DESC LIMIT 1",
(work_id, target_chapter),
).fetchone()
source_status = "active" if outline_row and outline_row[0] == "confirmed" else "stale"
# 授权快照以冻结行为准重读:快照 ID 必须与候选绑定一致,且核验时间不晚于本次检查。
auth = auth_payload if isinstance(auth_payload, Mapping) else {}
if isinstance(auth_payload, str):
try:
auth = json.loads(auth_payload)
except ValueError:
auth = {}
context_snapshot = context.get("authorizationSnapshot", {})
context_snapshot_id = context_snapshot.get("snapshotId") if isinstance(context_snapshot, Mapping) else None
authorization_valid = bool(
auth.get("snapshotId")
and context_snapshot_id
and auth.get("snapshotId") == context_snapshot_id
)
if authorization_valid:
verified_at = _parse_tz(auth.get("verifiedAt"))
authorization_valid = verified_at is not None and verified_at <= checked_at
# WriterContext 合同里 generatedAt 住在 contextSnapshot 内,顶层没有该字段
generated_at = _parse_tz(context.get("contextSnapshot", {}).get("generatedAt"))
if generated_at is None and freeze_time is not None:
generated_at = freeze_time.replace(tzinfo=timezone.utc)
if generated_at is None:
raise LiveStateError("无法确定候选生成时间,接受窗口不可计算")
expires_at = generated_at + ACCEPTANCE_WINDOW
return {
"qualityPolicyVersion": PRODUCTION_POLICY,
"contextSnapshotId": "sha256:" + str(manifest_sha),
"contextSnapshotSha256": "sha256:" + str(freeze_context_sha),
"authorizationSnapshotId": str(auth.get("snapshotId") or ""),
"authorizationValid": authorization_valid,
"sourceStatus": source_status,
"candidateExpiresAt": _iso_z(expires_at),
"checkedAt": _iso_z(checked_at),
"canonicalRevision": canonical_revision,
}
__all__ = ["ACCEPTANCE_WINDOW", "LiveStateError", "PRODUCTION_POLICY", "build_live_acceptance_state"]