feat(awooop): harden outbound truth chain mirror
Some checks failed
Code Review / ai-code-review (push) Successful in 10s
run-migration / migrate (push) Failing after 8s
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 3m27s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s
Some checks failed
Code Review / ai-code-review (push) Successful in 10s
run-migration / migrate (push) Failing after 8s
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 3m27s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s
This commit is contained in:
@@ -534,6 +534,9 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
|
||||
message_type,
|
||||
content_hash,
|
||||
content_preview,
|
||||
content_redacted,
|
||||
redaction_version,
|
||||
source_envelope,
|
||||
provider_message_id,
|
||||
send_status,
|
||||
queued_at,
|
||||
|
||||
@@ -39,13 +39,14 @@ from sqlalchemy import select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.db.awooop_models import AwoooPRunState
|
||||
from src.services.audit_sink import _redact_string
|
||||
from src.services.audit_sink import _redact_string, sanitize
|
||||
from src.services.platform_runtime import create_run
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Progressive Feedback Policy:等待超過此秒數才發 interim 訊息
|
||||
_INTERIM_WAIT_SECONDS = 30
|
||||
_OUTBOUND_REDACTION_VERSION = "audit_sink_v1"
|
||||
|
||||
|
||||
def _input_sha256(input_payload: dict[str, Any] | None) -> str | None:
|
||||
@@ -457,6 +458,7 @@ async def record_outbound_message(
|
||||
channel_chat_id: str,
|
||||
message_type: str, # 'interim' | 'final' | 'error' | 'approval_request'
|
||||
content: str | None = None,
|
||||
source_envelope: dict[str, Any] | None = None,
|
||||
provider_message_id: str | None = None,
|
||||
send_status: str = "pending",
|
||||
conversation_event_id: UUID | None = None,
|
||||
@@ -471,10 +473,20 @@ async def record_outbound_message(
|
||||
"""
|
||||
content_hash: str | None = None
|
||||
content_preview: str | None = None
|
||||
content_redacted: str | None = None
|
||||
if content is not None:
|
||||
content_hash = hashlib.sha256(content.encode()).hexdigest()
|
||||
redacted = _redact_string(content)
|
||||
content_preview = redacted[:256]
|
||||
content_redacted = _redact_string(content)
|
||||
content_preview = content_redacted[:256]
|
||||
|
||||
envelope: dict[str, Any] = sanitize(source_envelope or {})
|
||||
envelope.update({
|
||||
"schema_version": "outbound_source_envelope_v1",
|
||||
"redaction_version": _OUTBOUND_REDACTION_VERSION,
|
||||
"content_sha256": content_hash,
|
||||
"content_length": len(content) if content is not None else 0,
|
||||
})
|
||||
source_envelope_json = json.dumps(envelope, ensure_ascii=False, default=str)
|
||||
|
||||
actual_status = "shadow" if is_shadow else send_status
|
||||
|
||||
@@ -499,13 +511,17 @@ async def record_outbound_message(
|
||||
INSERT INTO awooop_outbound_message (
|
||||
project_id, run_id, conversation_event_id,
|
||||
channel_type, channel_chat_id, message_type,
|
||||
content_hash, content_preview, provider_message_id,
|
||||
content_hash, content_preview, content_redacted,
|
||||
redaction_version, source_envelope,
|
||||
provider_message_id,
|
||||
send_status, queued_at,
|
||||
triggered_by_state, waiting_since
|
||||
) VALUES (
|
||||
:project_id, :run_id, :conversation_event_id,
|
||||
:channel_type, :channel_chat_id, :message_type,
|
||||
:content_hash, :content_preview, :provider_message_id,
|
||||
:content_hash, :content_preview, :content_redacted,
|
||||
:redaction_version, CAST(:source_envelope AS jsonb),
|
||||
:provider_message_id,
|
||||
:send_status, NOW(),
|
||||
:triggered_by_state, :waiting_since
|
||||
)
|
||||
@@ -520,6 +536,9 @@ async def record_outbound_message(
|
||||
"message_type": message_type,
|
||||
"content_hash": content_hash,
|
||||
"content_preview": content_preview,
|
||||
"content_redacted": content_redacted,
|
||||
"redaction_version": _OUTBOUND_REDACTION_VERSION,
|
||||
"source_envelope": source_envelope_json,
|
||||
"provider_message_id": provider_message_id,
|
||||
"send_status": actual_status,
|
||||
"triggered_by_state": triggered_by_state,
|
||||
|
||||
@@ -25,6 +25,7 @@ SOUL.md 鐵律 (4.1 Telegram 訊息壓縮原則):
|
||||
import asyncio
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
@@ -119,6 +120,54 @@ def _infer_outbound_message_type(text: str, payload: dict) -> str:
|
||||
return "error"
|
||||
return "final"
|
||||
|
||||
|
||||
def _outbound_payload_hash(payload: dict) -> str:
|
||||
"""Stable hash for Telegram payload replay without storing raw payload."""
|
||||
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str)
|
||||
return hashlib.sha256(canonical.encode()).hexdigest()
|
||||
|
||||
|
||||
def _reply_markup_summary(payload: dict) -> dict[str, object]:
|
||||
"""Summarize Telegram buttons without turning callback payloads into policy."""
|
||||
reply_markup = payload.get("reply_markup")
|
||||
if not isinstance(reply_markup, dict):
|
||||
return {"present": False, "button_count": 0}
|
||||
|
||||
buttons: list[dict[str, object]] = []
|
||||
for row in reply_markup.get("inline_keyboard") or []:
|
||||
if not isinstance(row, list):
|
||||
continue
|
||||
for button in row:
|
||||
if not isinstance(button, dict):
|
||||
continue
|
||||
callback_data = str(button.get("callback_data") or "")
|
||||
buttons.append({
|
||||
"text": str(button.get("text") or ""),
|
||||
"callback_prefix": callback_data.split(":", 1)[0] if callback_data else "",
|
||||
"has_url": bool(button.get("url")),
|
||||
})
|
||||
|
||||
return {
|
||||
"present": True,
|
||||
"button_count": len(buttons),
|
||||
"buttons": buttons[:12],
|
||||
"truncated": len(buttons) > 12,
|
||||
}
|
||||
|
||||
|
||||
def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
|
||||
"""Build a redaction-friendly source envelope for Channel Hub replay."""
|
||||
return {
|
||||
"adapter": "legacy_telegram_gateway",
|
||||
"method": method,
|
||||
"payload_sha256": _outbound_payload_hash(payload),
|
||||
"payload_keys": sorted(str(key) for key in payload.keys()),
|
||||
"parse_mode": payload.get("parse_mode"),
|
||||
"disable_web_page_preview": payload.get("disable_web_page_preview"),
|
||||
"has_reply_context": _has_reply_context(payload),
|
||||
"reply_markup": _reply_markup_summary(payload),
|
||||
}
|
||||
|
||||
# 2026-04-27 Claude Sonnet 4.6: B3 — LLM 動態 Telegram 按鈕 Feature Flag
|
||||
# true → 優先使用 ActionPlan.recommended_actions 動態生成按鈕
|
||||
# false → 維持現有 callback_action_spec.yaml 路徑(預設,向下相容)
|
||||
@@ -1678,6 +1727,7 @@ class TelegramGateway:
|
||||
channel_chat_id=chat_id,
|
||||
message_type=_infer_outbound_message_type(text, payload),
|
||||
content=text,
|
||||
source_envelope=_outbound_source_envelope(method, payload),
|
||||
provider_message_id=provider_message_id,
|
||||
send_status="sent",
|
||||
triggered_by_state="legacy_gateway",
|
||||
|
||||
Reference in New Issue
Block a user