feat: 新增反馈渠道字段、插件状态表与日志敏感信息脱敏
1. 为消息反馈表新增channel字段并完善相关接口返回 2. 创建渠道插件状态存储表与对应索引 3. 调整路由规则agent_config_id字段类型为字符串 4. 添加日志敏感信息脱敏功能 5. 优化消息记录仓库方法,新增响应时间统计和查询接口
This commit is contained in:
parent
dd0ba81f66
commit
03d9dd0543
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.channels.models import ChannelMessage, ChannelResponse
|
||||
@ -43,17 +43,21 @@ class ChannelMessageRecordRepository:
|
||||
await self.db.refresh(record)
|
||||
return record
|
||||
|
||||
async def mark_success(self, record_id: int, response: ChannelResponse) -> None:
|
||||
await self.db.execute(
|
||||
update(ChannelMsgRecord)
|
||||
.where(ChannelMsgRecord.id == record_id)
|
||||
.values(
|
||||
status="success",
|
||||
reply_message_id=response.identity.channel_message_id,
|
||||
reply_content_preview=self._truncate(response.content, MAX_REPLY_PREVIEW),
|
||||
replied_at=utc_now_naive(),
|
||||
)
|
||||
)
|
||||
async def mark_success(
|
||||
self,
|
||||
record_id: int,
|
||||
response: ChannelResponse,
|
||||
response_time_ms: int | None = None,
|
||||
) -> None:
|
||||
values: dict = {
|
||||
"status": "success",
|
||||
"reply_message_id": response.identity.channel_message_id,
|
||||
"reply_content_preview": self._truncate(response.content, MAX_REPLY_PREVIEW),
|
||||
"replied_at": utc_now_naive(),
|
||||
}
|
||||
if response_time_ms is not None:
|
||||
values["response_time_ms"] = response_time_ms
|
||||
await self.db.execute(update(ChannelMsgRecord).where(ChannelMsgRecord.id == record_id).values(**values))
|
||||
await self.db.commit()
|
||||
|
||||
async def mark_error(self, record_id: int, error_message: str) -> None:
|
||||
@ -91,6 +95,33 @@ class ChannelMessageRecordRepository:
|
||||
cutoff = utc_now_naive() - timedelta(seconds=timeout_seconds)
|
||||
return [r for r in records if r.created_at and r.created_at < cutoff]
|
||||
|
||||
async def get_recent_records(
|
||||
self, channel_id: str, chat_id: str, limit: int = 10
|
||||
) -> list[ChannelMsgRecord]:
|
||||
result = await self.db.execute(
|
||||
select(ChannelMsgRecord)
|
||||
.where(
|
||||
ChannelMsgRecord.channel_id == channel_id,
|
||||
ChannelMsgRecord.chat_id == chat_id,
|
||||
)
|
||||
.order_by(ChannelMsgRecord.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_chat_message_count(
|
||||
self, channel_id: str, chat_id: str, since_hours: int = 24
|
||||
) -> int:
|
||||
cutoff = utc_now_naive() - timedelta(hours=since_hours)
|
||||
result = await self.db.execute(
|
||||
select(func.count()).select_from(ChannelMsgRecord).where(
|
||||
ChannelMsgRecord.channel_id == channel_id,
|
||||
ChannelMsgRecord.chat_id == chat_id,
|
||||
ChannelMsgRecord.created_at >= cutoff,
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
def _truncate(text: str, max_length: int) -> str:
|
||||
if len(text) <= max_length:
|
||||
|
||||
@ -12,6 +12,7 @@ async def submit_message_feedback_view(
|
||||
message_id: int,
|
||||
rating: str,
|
||||
reason: str | None,
|
||||
channel: str = "webchat",
|
||||
db: AsyncSession,
|
||||
current_user_id: str,
|
||||
) -> dict:
|
||||
@ -41,6 +42,7 @@ async def submit_message_feedback_view(
|
||||
user_id=str(current_user_id),
|
||||
rating=rating,
|
||||
reason=reason,
|
||||
channel=channel,
|
||||
)
|
||||
|
||||
db.add(new_feedback)
|
||||
@ -54,6 +56,7 @@ async def submit_message_feedback_view(
|
||||
"message_id": new_feedback.message_id,
|
||||
"rating": new_feedback.rating,
|
||||
"reason": new_feedback.reason,
|
||||
"channel": new_feedback.channel,
|
||||
"created_at": new_feedback.created_at.isoformat(),
|
||||
}
|
||||
|
||||
@ -86,6 +89,7 @@ async def get_message_feedback_view(
|
||||
"id": feedback.id,
|
||||
"rating": feedback.rating,
|
||||
"reason": feedback.reason,
|
||||
"channel": feedback.channel,
|
||||
"created_at": feedback.created_at.isoformat(),
|
||||
},
|
||||
}
|
||||
|
||||
@ -364,6 +364,24 @@ class PostgresManager(metaclass=SingletonMeta):
|
||||
"CREATE INDEX IF NOT EXISTS idx_msg_records_stuck"
|
||||
" ON channels_msg_records(channel_id, created_at) WHERE status = 'processing'",
|
||||
"CREATE INDEX IF NOT EXISTS idx_msg_records_chat ON channels_msg_records(channel_id, chat_id)",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS channel_plugin_states (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
channel_id VARCHAR(32) NOT NULL,
|
||||
namespace VARCHAR(64) NOT NULL DEFAULT 'default',
|
||||
entry_key VARCHAR(128) NOT NULL,
|
||||
value_json JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_channel_state UNIQUE (channel_id, namespace, entry_key)
|
||||
)
|
||||
""",
|
||||
"CREATE INDEX IF NOT EXISTS idx_plugin_states_expiry"
|
||||
" ON channel_plugin_states(expires_at) WHERE expires_at IS NOT NULL",
|
||||
"CREATE INDEX IF NOT EXISTS idx_plugin_states_channel ON channel_plugin_states(channel_id, namespace)",
|
||||
"ALTER TABLE IF EXISTS channels_routing_rules"
|
||||
" ALTER COLUMN agent_config_id TYPE VARCHAR(64) USING agent_config_id::VARCHAR",
|
||||
]
|
||||
async with self.async_engine.begin() as conn:
|
||||
for stmt in stmts:
|
||||
|
||||
@ -412,6 +412,9 @@ class MessageFeedback(Base):
|
||||
user_id = Column(String(64), nullable=False, index=True, comment="User ID who provided feedback")
|
||||
rating = Column(String(10), nullable=False, comment="Feedback rating: like or dislike")
|
||||
reason = Column(Text, nullable=True, comment="Optional reason for dislike feedback")
|
||||
channel = Column(
|
||||
String(32), nullable=False, default="webchat", comment="Feedback source channel: webchat/wechat/telegram/..."
|
||||
)
|
||||
created_at = Column(DateTime, default=utc_now_naive, comment="Feedback creation time")
|
||||
|
||||
# Relationships
|
||||
@ -424,6 +427,7 @@ class MessageFeedback(Base):
|
||||
"user_id": self.user_id,
|
||||
"rating": self.rating,
|
||||
"reason": self.reason,
|
||||
"channel": self.channel,
|
||||
"created_at": format_utc_datetime(self.created_at),
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""多渠道网关 ORM 模型 - 渠道用户映射、会话映射、消息记录"""
|
||||
"""多渠道网关 ORM 模型 - 渠道用户映射、会话映射、消息记录、插件状态"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
@ -10,6 +10,7 @@ from sqlalchemy import (
|
||||
Integer,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from yuxi.storage.postgres.models_business import Base
|
||||
@ -181,7 +182,7 @@ class ChannelRoutingRule(Base):
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
channel_id = Column(String(32), nullable=False)
|
||||
command = Column(String(64), nullable=False)
|
||||
agent_config_id = Column(Integer, nullable=False)
|
||||
agent_config_id = Column(String(64), nullable=False)
|
||||
priority = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, default=utc_now_naive)
|
||||
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
@ -201,3 +202,36 @@ class ChannelRoutingRule(Base):
|
||||
"created_at": format_utc_datetime(self.created_at),
|
||||
"updated_at": format_utc_datetime(self.updated_at),
|
||||
}
|
||||
|
||||
|
||||
class ChannelPluginState(Base):
|
||||
"""渠道插件运行时状态统一存储"""
|
||||
|
||||
__tablename__ = "channel_plugin_states"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
channel_id = Column(String(32), nullable=False)
|
||||
namespace = Column(String(64), nullable=False, default="default")
|
||||
entry_key = Column(String(128), nullable=False)
|
||||
value_json = Column(JSONB, nullable=False, default=dict)
|
||||
created_at = Column(DateTime, nullable=False, default=utc_now_naive)
|
||||
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
expires_at = Column(DateTime, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("channel_id", "namespace", "entry_key", name="uq_channel_state"),
|
||||
Index("idx_plugin_states_expiry", "expires_at", postgresql_where=(text("expires_at IS NOT NULL"))),
|
||||
Index("idx_plugin_states_channel", "channel_id", "namespace"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"channel_id": self.channel_id,
|
||||
"namespace": self.namespace,
|
||||
"entry_key": self.entry_key,
|
||||
"value_json": self.value_json or {},
|
||||
"created_at": format_utc_datetime(self.created_at),
|
||||
"updated_at": format_utc_datetime(self.updated_at),
|
||||
"expires_at": format_utc_datetime(self.expires_at) if self.expires_at else None,
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from loguru import logger as loguru_logger
|
||||
@ -10,6 +11,25 @@ SAVE_DIR = os.getenv("SAVE_DIR") or "saves"
|
||||
DATETIME = shanghai_now().strftime("%Y-%m-%d")
|
||||
LOG_FILE = f"{SAVE_DIR}/logs/yuxi-{DATETIME}.log"
|
||||
|
||||
_SENSITIVE_RE_PATTERNS = [
|
||||
(re.compile(r'(token[=:]\s*["\']?)([^\s"\'&,]+)', re.IGNORECASE), r"\1***"),
|
||||
(re.compile(r'(secret[=:]\s*["\']?)([^\s"\'&,]+)', re.IGNORECASE), r"\1***"),
|
||||
(re.compile(r'(password[=:]\s*["\']?)([^\s"\'&,]+)', re.IGNORECASE), r"\1***"),
|
||||
(re.compile(r'(api_key[=:]\s*["\']?)([^\s"\'&,]+)', re.IGNORECASE), r"\1***"),
|
||||
(re.compile(r"(Authorization:\s*)(Bearer\s+\S+)", re.IGNORECASE), r"\1***"),
|
||||
(re.compile(r"(X-API-Key:\s*)(\S+)", re.IGNORECASE), r"\1***"),
|
||||
(re.compile(r"'(app_secret|app_key|bot_token|access_token)':\s*'([^']+)'"), r"'\1': '***'"),
|
||||
(re.compile(r'"(app_secret|app_key|bot_token|access_token)":\s*"([^"]+)"'), r'"\1": "***"'),
|
||||
]
|
||||
|
||||
|
||||
def _redact_sensitive_in_message(record: dict) -> None:
|
||||
message = record.get("message", "")
|
||||
if isinstance(message, str):
|
||||
for pattern, replacement in _SENSITIVE_RE_PATTERNS:
|
||||
message = pattern.sub(replacement, message)
|
||||
record["message"] = message
|
||||
|
||||
|
||||
class LoguruHandler(logging.Handler):
|
||||
"""将 Python logging 桥接到 loguru 的 handler"""
|
||||
@ -69,6 +89,7 @@ def setup_logger(name, level="DEBUG", console=True):
|
||||
retention="30 days",
|
||||
compression="zip",
|
||||
enqueue=True,
|
||||
filter=_redact_sensitive_in_message,
|
||||
)
|
||||
|
||||
# 添加控制台日志(有颜色)
|
||||
@ -84,6 +105,7 @@ def setup_logger(name, level="DEBUG", console=True):
|
||||
),
|
||||
colorize=True,
|
||||
enqueue=True,
|
||||
filter=_redact_sensitive_in_message,
|
||||
)
|
||||
|
||||
return loguru_logger
|
||||
|
||||
Loading…
Reference in New Issue
Block a user