feedback-issue: 拆三步、入口意图门、消息可靠性、日志脱敏与噪音过滤 (#5810)

This commit is contained in:
InfinityPacer
2026-05-21 13:57:12 +08:00
committed by GitHub
parent 4c64b1769d
commit 0245c8db80
13 changed files with 2917 additions and 150 deletions

View File

@@ -0,0 +1,261 @@
"""反馈 Issue 流程的短期服务端状态。
这里保存两类只应由工具写入的状态:
- 诊断日志收集结果:证明 Agent 在提交前尝试读取过本地日志。
- 用户确认结果:证明用户通过按钮确认过某份预览草稿。
状态只保存在当前进程内,重启后失效;这符合反馈提交这种交互式流程的预期,
也避免把一次性确认 token 持久化到数据库。
"""
from __future__ import annotations
import hashlib
import time
import uuid
from dataclasses import dataclass
from threading import Lock
from typing import Optional
FEEDBACK_CONFIRM_VALUE_PREFIX = "__feedback_issue_confirm__:"
_STATE_TTL_SECONDS = 60 * 60
@dataclass
class FeedbackDiagnosticsRecord:
"""一次反馈诊断日志收集结果。"""
diagnostics_id: str
session_id: str
user_id: str
username: Optional[str]
logs: str
source_files: list[str]
found: bool
created_at: float
@dataclass
class FeedbackConfirmationRecord:
"""一次反馈 Issue 预览确认状态。"""
confirmation_token: str
session_id: str
user_id: str
username: Optional[str]
draft_hash: str
diagnostics_id: str
created_at: float
confirmed_at: Optional[float] = None
def build_feedback_draft_hash(
*,
title: str,
version: str,
environment: str,
issue_type: str,
description: str,
original_user_request: str,
logs: Optional[str],
diagnostics_id: str,
) -> str:
"""为用户确认的 Issue 草稿生成稳定摘要。"""
parts = (
title.strip(),
version.strip(),
environment.strip(),
issue_type.strip(),
description.strip(),
original_user_request.strip(),
(logs or "").strip(),
diagnostics_id.strip(),
)
return hashlib.sha256("\x00".join(parts).encode("utf-8", errors="replace")).hexdigest()
class FeedbackIssueStateStore:
"""管理反馈 Issue 流程的进程内短期状态。"""
def __init__(self) -> None:
self._diagnostics: dict[str, FeedbackDiagnosticsRecord] = {}
self._confirmations: dict[str, FeedbackConfirmationRecord] = {}
self._lock = Lock()
def _cleanup_locked(self) -> None:
expire_before = time.time() - _STATE_TTL_SECONDS
for diagnostics_id, record in list(self._diagnostics.items()):
if record.created_at < expire_before:
self._diagnostics.pop(diagnostics_id, None)
for token, record in list(self._confirmations.items()):
if record.created_at < expire_before:
self._confirmations.pop(token, None)
def create_diagnostics(
self,
*,
session_id: str,
user_id: str,
username: Optional[str],
logs: str,
source_files: list[str],
found: bool,
) -> FeedbackDiagnosticsRecord:
"""登记一次日志收集结果。"""
with self._lock:
self._cleanup_locked()
diagnostics_id = uuid.uuid4().hex[:12]
while diagnostics_id in self._diagnostics:
diagnostics_id = uuid.uuid4().hex[:12]
record = FeedbackDiagnosticsRecord(
diagnostics_id=diagnostics_id,
session_id=session_id,
user_id=str(user_id),
username=username,
logs=logs,
source_files=source_files,
found=found,
created_at=time.time(),
)
self._diagnostics[diagnostics_id] = record
return record
def get_diagnostics(
self,
diagnostics_id: str,
*,
session_id: str,
user_id: str,
) -> Optional[FeedbackDiagnosticsRecord]:
"""按会话和用户读取诊断结果,防止跨用户复用。"""
with self._lock:
self._cleanup_locked()
record = self._diagnostics.get(diagnostics_id)
if not record:
return None
if record.session_id != session_id or record.user_id != str(user_id):
return None
return record
def find_active_confirmation(
self,
*,
session_id: str,
user_id: str,
) -> Optional[FeedbackConfirmationRecord]:
"""查找当前会话/用户尚未消费、且未点击确认的预览 token。
prepare_feedback_issue 会用它判断「上一份预览还挂着,不该再发一份」,
避免 #5806 实测里发了两次同样的确认按钮、用户点了两次的情况。"""
with self._lock:
self._cleanup_locked()
for record in self._confirmations.values():
if (
record.session_id == session_id
and record.user_id == str(user_id)
and record.confirmed_at is None
):
return record
return None
def invalidate_active_confirmations(
self,
*,
session_id: str,
user_id: str,
) -> int:
"""作废当前会话所有未确认的预览 token返回作废数量。
用户在 prepare 之后修改草稿、重新调 prepare 时调用;旧 token 失效
后即便残留消息里的按钮被点击,``mark_confirmed`` 也会因找不到记录
而返回 False避免脏数据驱动提交。"""
with self._lock:
self._cleanup_locked()
to_drop = [
token
for token, record in self._confirmations.items()
if record.session_id == session_id
and record.user_id == str(user_id)
and record.confirmed_at is None
]
for token in to_drop:
self._confirmations.pop(token, None)
return len(to_drop)
def create_confirmation(
self,
*,
session_id: str,
user_id: str,
username: Optional[str],
draft_hash: str,
diagnostics_id: str,
) -> FeedbackConfirmationRecord:
"""创建待用户点击确认的草稿 token。"""
with self._lock:
self._cleanup_locked()
token = uuid.uuid4().hex
while token in self._confirmations:
token = uuid.uuid4().hex
record = FeedbackConfirmationRecord(
confirmation_token=token,
session_id=session_id,
user_id=str(user_id),
username=username,
draft_hash=draft_hash,
diagnostics_id=diagnostics_id,
created_at=time.time(),
)
self._confirmations[token] = record
return record
def mark_confirmed(
self,
token: str,
*,
session_id: str,
user_id: str,
) -> bool:
"""按钮回调命中时,把 token 标记为已由用户确认。"""
with self._lock:
self._cleanup_locked()
record = self._confirmations.get(token)
if not record:
return False
if record.session_id != session_id or record.user_id != str(user_id):
return False
record.confirmed_at = time.time()
return True
def consume_confirmed(
self,
token: str,
*,
session_id: str,
user_id: str,
draft_hash: str,
) -> Optional[FeedbackConfirmationRecord]:
"""消费一次已确认 token内容摘要不一致时拒绝。"""
with self._lock:
self._cleanup_locked()
record = self._confirmations.get(token)
if not record:
return None
if (
record.session_id != session_id
or record.user_id != str(user_id)
or record.draft_hash != draft_hash
or record.confirmed_at is None
):
return None
return self._confirmations.pop(token, None)
def clear(self) -> None:
"""测试和重置场景使用:清空所有短期状态。"""
with self._lock:
self._diagnostics.clear()
self._confirmations.clear()
feedback_issue_state_store = FeedbackIssueStateStore()