refactor: 批量清理冗余空行,优化部分枚举使用方式
1. 移除所有适配器文件中多余的空导入行 2. 调整ValidationError继承,移除不必要的ValueError继承 3. 修正多处ChannelType使用方式,从.value改为直接使用枚举实例 4. 优化飞书插件部分硬编码渠道类型为枚举实例 5. 更新wechat_ilink插件清单与适配器配置 6. 新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断 7. 优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
This commit is contained in:
parent
29a45916ff
commit
8eead29de0
@ -44,22 +44,22 @@ from .agent_run_adapter import AgentRunAdapter
|
||||
from .agent_run_execution_adapter import AgentRunExecutionAdapter
|
||||
from .arq_queue_adapter import ARQQueueAdapter
|
||||
from .channel_persistence_adapter import ChannelPersistenceAdapter
|
||||
from .conversation_adapter import ConversationAdapter
|
||||
from .identity_resolver_adapter import IdentityResolverAdapter
|
||||
from .masking_adapter import MaskingAdapter
|
||||
from .opentelemetry_tracer_adapter import OpenTelemetryTracerAdapter
|
||||
from .redis_cache_adapter import RedisCacheAdapter
|
||||
from .redis_config_adapter import RedisConfigAdapter
|
||||
from .service_account_adapter import ServiceAccountAdapter
|
||||
from .sqlalchemy_transaction_adapter import SqlAlchemyTransactionAdapter
|
||||
from .structured_logger_adapter import StructuredLoggerAdapter
|
||||
|
||||
# 独立装配路径适配器(5 个,不经过工厂,不进入 DrivenAdapters 聚合)
|
||||
from .content_review_repository_adapter import ContentReviewRepositoryAdapter
|
||||
from .conversation_adapter import ConversationAdapter
|
||||
from .default_content_moderation_adapter import DefaultContentModerationAdapter
|
||||
from .identity_resolver_adapter import IdentityResolverAdapter
|
||||
from .inmemory_realtime_metrics_adapter import InmemoryRealtimeMetricsAdapter
|
||||
from .masking_adapter import MaskingAdapter
|
||||
from .message_ops_port_adapter import MessageOpsPortAdapter
|
||||
from .opentelemetry_tracer_adapter import OpenTelemetryTracerAdapter
|
||||
from .redis_cache_adapter import RedisCacheAdapter
|
||||
from .redis_config_adapter import RedisConfigAdapter
|
||||
from .report_repository_adapter import SqlAlchemyReportRepositoryAdapter
|
||||
from .service_account_adapter import ServiceAccountAdapter
|
||||
from .sqlalchemy_transaction_adapter import SqlAlchemyTransactionAdapter
|
||||
from .structured_logger_adapter import StructuredLoggerAdapter
|
||||
|
||||
# 无状态适配器全局实例(Masking / Logger / Tracer),跨请求复用,避免重复构造。
|
||||
# Masking 无状态无依赖,最先构造;Logger 注入 Masking 以便日志 kwargs 自动脱敏;
|
||||
|
||||
@ -163,7 +163,7 @@ class AgentRunAdapter(AgentRunPort):
|
||||
await self._logger.error(
|
||||
"service_account_resolve_failed",
|
||||
resource="service_account",
|
||||
channel_type=ctx.channel_type.value,
|
||||
channel_type=ctx.channel_type,
|
||||
account_id=ctx.channel_account_id,
|
||||
error=str(exc),
|
||||
)
|
||||
@ -177,7 +177,7 @@ class AgentRunAdapter(AgentRunPort):
|
||||
current_uid = ""
|
||||
if ctx is not None:
|
||||
if ctx.channel_type is not None:
|
||||
meta["channel_type"] = ctx.channel_type.value
|
||||
meta["channel_type"] = ctx.channel_type
|
||||
meta["channel_account_id"] = ctx.channel_account_id
|
||||
meta["channel_peer_id"] = ctx.channel_peer_id
|
||||
meta["channel_group_id"] = ctx.channel_group_id
|
||||
|
||||
@ -306,7 +306,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
commit = self._should_commit(tx)
|
||||
data: dict[str, Any] = {
|
||||
"channel_type": cmd.channel_type.value,
|
||||
"channel_type": cmd.channel_type,
|
||||
"account_id": cmd.account_id,
|
||||
"display_name": cmd.display_name,
|
||||
"config": cmd.config,
|
||||
@ -359,7 +359,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
commit = self._should_commit(tx)
|
||||
try:
|
||||
orm = await self._repos.account.get_by_type_and_account(cmd.channel_type.value, cmd.account_id)
|
||||
orm = await self._repos.account.get_by_type_and_account(cmd.channel_type, cmd.account_id)
|
||||
if orm is None:
|
||||
raise NotFoundError("channel_account", cmd.account_id)
|
||||
data: dict = {}
|
||||
@ -411,7 +411,7 @@ class ChannelPersistenceAdapter(
|
||||
DependencyError: 数据库故障。
|
||||
"""
|
||||
try:
|
||||
orm = await self._repos.account.get_by_type_and_account(channel_type.value, account_id)
|
||||
orm = await self._repos.account.get_by_type_and_account(channel_type, account_id)
|
||||
if orm is None:
|
||||
return None
|
||||
return orm_to_channel_account(orm)
|
||||
@ -446,7 +446,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
try:
|
||||
orms = await self._repos.account.list(
|
||||
channel_type=channel_type.value if channel_type else None,
|
||||
channel_type=channel_type if channel_type else None,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
@ -486,7 +486,7 @@ class ChannelPersistenceAdapter(
|
||||
try:
|
||||
stmt = select(ChannelAccountORM).where(ChannelAccountORM.is_deleted == 0)
|
||||
if filter.channel_type is not None:
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == filter.channel_type.value)
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == filter.channel_type)
|
||||
if filter.status is not None:
|
||||
stmt = stmt.where(ChannelAccountORM.status == filter.status.value)
|
||||
result = await self._db.execute(stmt)
|
||||
@ -528,7 +528,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
commit = self._should_commit(tx)
|
||||
try:
|
||||
orm = await self._repos.account.get_by_type_and_account(channel_type.value, account_id)
|
||||
orm = await self._repos.account.get_by_type_and_account(channel_type, account_id)
|
||||
if orm is None:
|
||||
return False
|
||||
await self._repos.account.delete_by_id(orm.id, commit=commit)
|
||||
@ -563,7 +563,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
commit = self._should_commit(tx)
|
||||
try:
|
||||
account_orm = await self._repos.account.get_by_type_and_account(cmd.channel_type.value, cmd.account_id)
|
||||
account_orm = await self._repos.account.get_by_type_and_account(cmd.channel_type, cmd.account_id)
|
||||
if account_orm is None:
|
||||
raise NotFoundError("channel_account", cmd.account_id)
|
||||
conversation_id_int: int | None = None
|
||||
@ -575,7 +575,7 @@ class ChannelPersistenceAdapter(
|
||||
data = {
|
||||
"session_id": uuid.uuid4().hex,
|
||||
"account_id": account_orm.id,
|
||||
"channel_type": cmd.channel_type.value,
|
||||
"channel_type": cmd.channel_type,
|
||||
"peer_id": cmd.peer_id,
|
||||
"chat_type": cmd.chat_type,
|
||||
"conversation_id": conversation_id_int,
|
||||
@ -703,7 +703,7 @@ class ChannelPersistenceAdapter(
|
||||
DependencyError: 数据库故障。
|
||||
"""
|
||||
try:
|
||||
account_orm = await self._repos.account.get_by_type_and_account(channel_type.value, account_id)
|
||||
account_orm = await self._repos.account.get_by_type_and_account(channel_type, account_id)
|
||||
if account_orm is None:
|
||||
return None
|
||||
orm = await self._repos.session.get_by_account_and_peer(account_orm.id, peer_id)
|
||||
@ -837,7 +837,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
try:
|
||||
orms = await self._repos.session.list(
|
||||
channel_type=channel_type.value if channel_type else None,
|
||||
channel_type=channel_type if channel_type else None,
|
||||
peer_id=peer_id,
|
||||
start_time=created_after,
|
||||
end_time=created_before,
|
||||
@ -889,7 +889,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
try:
|
||||
return await self._repos.session.count(
|
||||
channel_type=channel_type.value if channel_type else None,
|
||||
channel_type=channel_type if channel_type else None,
|
||||
peer_id=peer_id,
|
||||
start_time=created_after,
|
||||
end_time=created_before,
|
||||
@ -938,7 +938,7 @@ class ChannelPersistenceAdapter(
|
||||
try:
|
||||
stmt = select(ChannelSessionORM).where(ChannelSessionORM.is_deleted == 0)
|
||||
if filter.channel_type is not None:
|
||||
stmt = stmt.where(ChannelSessionORM.channel_type == filter.channel_type.value)
|
||||
stmt = stmt.where(ChannelSessionORM.channel_type == filter.channel_type)
|
||||
if filter.unified_identity_id is not None:
|
||||
stmt = stmt.where(ChannelSessionORM.unified_identity_id == filter.unified_identity_id)
|
||||
if filter.inactive_before is not None:
|
||||
@ -1117,7 +1117,7 @@ class ChannelPersistenceAdapter(
|
||||
"""
|
||||
commit = self._should_commit(tx)
|
||||
try:
|
||||
account_orm = await self._repos.account.get_by_type_and_account(cmd.channel_type.value, cmd.account_id)
|
||||
account_orm = await self._repos.account.get_by_type_and_account(cmd.channel_type, cmd.account_id)
|
||||
if account_orm is None:
|
||||
raise NotFoundError("channel_account", cmd.account_id)
|
||||
now = utc_now_naive()
|
||||
@ -1266,7 +1266,7 @@ class ChannelPersistenceAdapter(
|
||||
DependencyError: 数据库故障(fail-closed)。
|
||||
"""
|
||||
try:
|
||||
account_orm = await self._repos.account.get_by_type_and_account(channel_type.value, account_id)
|
||||
account_orm = await self._repos.account.get_by_type_and_account(channel_type, account_id)
|
||||
if account_orm is None:
|
||||
return None
|
||||
orm = await self._repos.pairing.get_active_by_account_and_peer(account_orm.id, peer_id)
|
||||
@ -1343,9 +1343,7 @@ class ChannelPersistenceAdapter(
|
||||
account_pks: list[int] | None = None
|
||||
if query.channel_type is not None and query.account_id is not None:
|
||||
# 按渠道类型 + 业务 account_id 定位单个账户
|
||||
account_orm = await self._repos.account.get_by_type_and_account(
|
||||
query.channel_type.value, query.account_id
|
||||
)
|
||||
account_orm = await self._repos.account.get_by_type_and_account(query.channel_type, query.account_id)
|
||||
if account_orm is None:
|
||||
return ()
|
||||
account_pks = [account_orm.id]
|
||||
@ -1362,7 +1360,7 @@ class ChannelPersistenceAdapter(
|
||||
elif query.channel_type is not None:
|
||||
# 仅按渠道类型过滤,查询该渠道下所有未删除账户
|
||||
stmt = select(ChannelAccountORM.id).where(
|
||||
ChannelAccountORM.channel_type == query.channel_type.value,
|
||||
ChannelAccountORM.channel_type == query.channel_type,
|
||||
ChannelAccountORM.is_deleted == 0,
|
||||
)
|
||||
result = await self._db.execute(stmt)
|
||||
@ -1482,7 +1480,7 @@ class ChannelPersistenceAdapter(
|
||||
agg_stmt = agg_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelPairingORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type)
|
||||
agg_row = (await self._db.execute(agg_stmt)).one()
|
||||
total_requested = int(agg_row.total_requested or 0)
|
||||
approved_count = int(agg_row.approved_count or 0)
|
||||
@ -1515,7 +1513,7 @@ class ChannelPersistenceAdapter(
|
||||
trend_stmt = trend_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelPairingORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type)
|
||||
trend_result = await self._db.execute(trend_stmt)
|
||||
trend: list[PairingTrendPoint] = [
|
||||
PairingTrendPoint(
|
||||
@ -1679,7 +1677,7 @@ class ChannelPersistenceAdapter(
|
||||
self._repos.audit_log.list(
|
||||
operation=query.operation_type.value if query.operation_type is not None else None,
|
||||
operator=query.operator,
|
||||
target_channel=query.target_channel.value if query.target_channel is not None else None,
|
||||
target_channel=query.target_channel if query.target_channel is not None else None,
|
||||
target_account=query.target_account,
|
||||
start_time=_to_naive_utc(query.start_time),
|
||||
end_time=_to_naive_utc(query.end_time),
|
||||
@ -1728,7 +1726,7 @@ class ChannelPersistenceAdapter(
|
||||
self._repos.audit_log.count(
|
||||
operation=query.operation_type.value if query.operation_type is not None else None,
|
||||
operator=query.operator,
|
||||
target_channel=query.target_channel.value if query.target_channel is not None else None,
|
||||
target_channel=query.target_channel if query.target_channel is not None else None,
|
||||
target_account=query.target_account,
|
||||
start_time=_to_naive_utc(query.start_time),
|
||||
end_time=_to_naive_utc(query.end_time),
|
||||
@ -1843,7 +1841,7 @@ class ChannelPersistenceAdapter(
|
||||
self._repos.audit_log.get_stats(
|
||||
operation=query.operation_type.value if query.operation_type is not None else None,
|
||||
operator=query.operator,
|
||||
target_channel=query.target_channel.value if query.target_channel is not None else None,
|
||||
target_channel=query.target_channel if query.target_channel is not None else None,
|
||||
target_account=query.target_account,
|
||||
start_time=_to_naive_utc(query.start_time),
|
||||
end_time=_to_naive_utc(query.end_time),
|
||||
@ -2204,7 +2202,7 @@ class ChannelPersistenceAdapter(
|
||||
raise NotFoundError("message", str(message_pk))
|
||||
|
||||
return RetryContext(
|
||||
channel_type=account_dto.channel_type.value,
|
||||
channel_type=account_dto.channel_type,
|
||||
account_id=account_dto.account_id,
|
||||
peer_id=peer_id,
|
||||
message_content=message_orm.content,
|
||||
@ -2292,7 +2290,7 @@ class ChannelPersistenceAdapter(
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
)
|
||||
if query_filter.channel_type is not None:
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == query_filter.channel_type.value)
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == query_filter.channel_type)
|
||||
if query_filter.channel_account_id is not None:
|
||||
stmt = stmt.where(ChannelAccountORM.account_id == query_filter.channel_account_id)
|
||||
if query_filter.status is not None:
|
||||
@ -2456,7 +2454,7 @@ class ChannelPersistenceAdapter(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
)
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
status_counts: dict[str, int] = {
|
||||
"pending": 0,
|
||||
@ -2518,7 +2516,7 @@ class ChannelPersistenceAdapter(
|
||||
stmt = stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type)
|
||||
if query.created_after is not None:
|
||||
stmt = stmt.where(ChannelOutboxEntryORM.created_at >= _to_naive_utc(query.created_after))
|
||||
if query.created_before is not None:
|
||||
@ -2610,7 +2608,7 @@ class ChannelPersistenceAdapter(
|
||||
stmt = stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
return tuple(
|
||||
TrendDataPoint(
|
||||
@ -2652,7 +2650,7 @@ class ChannelPersistenceAdapter(
|
||||
.group_by(ChannelAccountORM.channel_type, ChannelAccountORM.status)
|
||||
)
|
||||
if channel_type is not None:
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
stmt = stmt.where(ChannelAccountORM.channel_type == channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
|
||||
by_channel: dict[str, int] = {}
|
||||
@ -2699,7 +2697,7 @@ class ChannelPersistenceAdapter(
|
||||
.group_by(ChannelSessionORM.channel_type, ChannelSessionORM.is_temporary)
|
||||
)
|
||||
if channel_type is not None:
|
||||
stmt = stmt.where(ChannelSessionORM.channel_type == channel_type.value)
|
||||
stmt = stmt.where(ChannelSessionORM.channel_type == channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
|
||||
by_channel: dict[str, int] = {}
|
||||
@ -2895,7 +2893,7 @@ class ChannelPersistenceAdapter(
|
||||
"identity_id": uuid.uuid4().hex,
|
||||
"identity_type": cmd.identity_type,
|
||||
"identity_value": cmd.identity_value,
|
||||
"channel_type": cmd.channel_type.value if cmd.channel_type is not None else None,
|
||||
"channel_type": cmd.channel_type if cmd.channel_type is not None else None,
|
||||
"channel_sender_id": cmd.channel_sender_id,
|
||||
"source": cmd.source,
|
||||
"user_id": int(cmd.user_id) if cmd.user_id else None,
|
||||
@ -3209,7 +3207,7 @@ class ChannelPersistenceAdapter(
|
||||
total_stmt = total_stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
total = int(await self._db.scalar(total_stmt) or 0)
|
||||
|
||||
# 时间序列
|
||||
@ -3227,7 +3225,7 @@ class ChannelPersistenceAdapter(
|
||||
ts_stmt = ts_stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
ts_result = await self._db.execute(ts_stmt)
|
||||
timeseries = tuple(
|
||||
TimeSeriesPoint(
|
||||
@ -3260,7 +3258,7 @@ class ChannelPersistenceAdapter(
|
||||
)
|
||||
)
|
||||
if channel_type is not None:
|
||||
combined_stmt = combined_stmt.where(ConversationORM.channel_type == channel_type.value)
|
||||
combined_stmt = combined_stmt.where(ConversationORM.channel_type == channel_type)
|
||||
combined_result = await self._db.execute(combined_stmt)
|
||||
by_channel: dict[str, int] = {}
|
||||
by_role: dict[str, int] = {}
|
||||
@ -3323,7 +3321,7 @@ class ChannelPersistenceAdapter(
|
||||
total_stmt = total_stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
total = int(await self._db.scalar(total_stmt) or 0)
|
||||
|
||||
# 按消息类型分组
|
||||
@ -3339,7 +3337,7 @@ class ChannelPersistenceAdapter(
|
||||
type_stmt = type_stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
type_result = await self._db.execute(type_stmt)
|
||||
by_type: dict[str, int] = {}
|
||||
for msg_type, count in type_result.all():
|
||||
@ -3394,7 +3392,7 @@ class ChannelPersistenceAdapter(
|
||||
ChannelSessionORM.is_deleted == 0,
|
||||
)
|
||||
if channel_type is not None:
|
||||
total_stmt = total_stmt.where(ChannelSessionORM.channel_type == channel_type.value)
|
||||
total_stmt = total_stmt.where(ChannelSessionORM.channel_type == channel_type)
|
||||
total = int(await self._db.scalar(total_stmt) or 0)
|
||||
|
||||
# 时间序列
|
||||
@ -3410,7 +3408,7 @@ class ChannelPersistenceAdapter(
|
||||
.order_by(bucket_expr)
|
||||
)
|
||||
if channel_type is not None:
|
||||
ts_stmt = ts_stmt.where(ChannelSessionORM.channel_type == channel_type.value)
|
||||
ts_stmt = ts_stmt.where(ChannelSessionORM.channel_type == channel_type)
|
||||
ts_result = await self._db.execute(ts_stmt)
|
||||
timeseries = tuple(
|
||||
TimeSeriesPoint(
|
||||
@ -3438,7 +3436,7 @@ class ChannelPersistenceAdapter(
|
||||
)
|
||||
)
|
||||
if channel_type is not None:
|
||||
combined_stmt = combined_stmt.where(ChannelSessionORM.channel_type == channel_type.value)
|
||||
combined_stmt = combined_stmt.where(ChannelSessionORM.channel_type == channel_type)
|
||||
combined_result = await self._db.execute(combined_stmt)
|
||||
by_channel: dict[str, int] = {}
|
||||
by_is_temporary: dict[str, int] = {"true": 0, "false": 0}
|
||||
@ -3480,7 +3478,7 @@ class ChannelPersistenceAdapter(
|
||||
.group_by(mc_bucket)
|
||||
)
|
||||
if channel_type is not None:
|
||||
mc_stmt = mc_stmt.where(ChannelSessionORM.channel_type == channel_type.value)
|
||||
mc_stmt = mc_stmt.where(ChannelSessionORM.channel_type == channel_type)
|
||||
mc_result = await self._db.execute(mc_stmt)
|
||||
by_message_count_bucket: dict[str, int] = {
|
||||
"0-10": 0,
|
||||
@ -3561,7 +3559,7 @@ class ChannelPersistenceAdapter(
|
||||
agg_stmt = agg_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == channel_type)
|
||||
agg_row = (await self._db.execute(agg_stmt)).one()
|
||||
total = int(agg_row.total or 0)
|
||||
success_count = int(agg_row.success or 0)
|
||||
@ -3594,7 +3592,7 @@ class ChannelPersistenceAdapter(
|
||||
retry_stmt = retry_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == channel_type)
|
||||
retry_result = await self._db.execute(retry_stmt)
|
||||
retry_distribution: dict[str, int] = {
|
||||
"0": 0,
|
||||
@ -3662,7 +3660,7 @@ class ChannelPersistenceAdapter(
|
||||
pct_stmt = pct_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == channel_type)
|
||||
pct_row = (await self._db.execute(pct_stmt)).one()
|
||||
p50 = float(pct_row.p50) if pct_row.p50 is not None else None
|
||||
p90 = float(pct_row.p90) if pct_row.p90 is not None else None
|
||||
@ -3691,7 +3689,7 @@ class ChannelPersistenceAdapter(
|
||||
hist_stmt = hist_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == channel_type)
|
||||
hist_result = await self._db.execute(hist_stmt)
|
||||
histogram: dict[str, int] = {
|
||||
"0-100ms": 0,
|
||||
@ -3783,7 +3781,7 @@ class ChannelPersistenceAdapter(
|
||||
agg_stmt = agg_stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == query.channel_type)
|
||||
if start_naive is not None:
|
||||
agg_stmt = agg_stmt.where(ChannelOutboxEntryORM.created_at >= start_naive)
|
||||
if end_naive is not None:
|
||||
@ -3897,7 +3895,7 @@ class ChannelPersistenceAdapter(
|
||||
stmt = stmt.join(
|
||||
ChannelAccountORM,
|
||||
ChannelOutboxEntryORM.account_id == ChannelAccountORM.id,
|
||||
).where(ChannelAccountORM.channel_type == channel_type.value)
|
||||
).where(ChannelAccountORM.channel_type == channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
counts = {"enter": 0, "sent": 0, "suppressed": 0, "failed": 0, "dead": 0}
|
||||
for node, count in result.all():
|
||||
@ -3977,7 +3975,7 @@ class ChannelPersistenceAdapter(
|
||||
)
|
||||
)
|
||||
if query.channel_type is not None:
|
||||
msg_stmt = msg_stmt.where(ConversationORM.channel_type == query.channel_type.value)
|
||||
msg_stmt = msg_stmt.where(ConversationORM.channel_type == query.channel_type)
|
||||
msg_result = await self._db.execute(msg_stmt)
|
||||
|
||||
# 会话维度聚合:JOIN ChannelSession → ChannelAccount,按业务 account_id 分组
|
||||
@ -4002,7 +4000,7 @@ class ChannelPersistenceAdapter(
|
||||
)
|
||||
)
|
||||
if query.channel_type is not None:
|
||||
session_stmt = session_stmt.where(ChannelAccountORM.channel_type == query.channel_type.value)
|
||||
session_stmt = session_stmt.where(ChannelAccountORM.channel_type == query.channel_type)
|
||||
session_result = await self._db.execute(session_stmt)
|
||||
session_map: dict[tuple[str, str], int] = {}
|
||||
for row in session_result.all():
|
||||
@ -4077,7 +4075,7 @@ class ChannelPersistenceAdapter(
|
||||
.order_by(bucket_expr)
|
||||
)
|
||||
if query.channel_type is not None:
|
||||
stmt = stmt.where(ConversationORM.channel_type == query.channel_type.value)
|
||||
stmt = stmt.where(ConversationORM.channel_type == query.channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
return tuple(
|
||||
TrendDataPoint(
|
||||
@ -4151,7 +4149,7 @@ class ChannelPersistenceAdapter(
|
||||
.limit(query.limit)
|
||||
)
|
||||
if query.channel_type is not None:
|
||||
stmt = stmt.where(ChannelSessionORM.channel_type == query.channel_type.value)
|
||||
stmt = stmt.where(ChannelSessionORM.channel_type == query.channel_type)
|
||||
if query.account_id is not None:
|
||||
stmt = stmt.join(
|
||||
ChannelAccountORM,
|
||||
|
||||
@ -140,7 +140,7 @@ def _record_to_orm_data(record: ContentReviewRecord) -> dict[str, Any]:
|
||||
"""
|
||||
return {
|
||||
"review_id": record.review_id,
|
||||
"channel_type": record.channel_type.value,
|
||||
"channel_type": record.channel_type,
|
||||
"account_id": record.account_id,
|
||||
"resource_type": record.resource_type.value,
|
||||
"content_preview": record.content_preview,
|
||||
@ -163,7 +163,7 @@ def _orm_to_history_item(orm: Any) -> ContentReviewHistoryItem:
|
||||
"""
|
||||
return ContentReviewHistoryItem(
|
||||
review_id=orm.review_id,
|
||||
channel_type=_enum(orm.channel_type, ChannelType, "content_review_record"),
|
||||
channel_type=ChannelType(orm.channel_type),
|
||||
account_id=orm.account_id,
|
||||
resource_type=_enum(orm.resource_type, ContentReviewResourceType, "content_review_record"),
|
||||
verdict=_enum(orm.verdict, ContentReviewVerdict, "content_review_record"),
|
||||
@ -184,7 +184,7 @@ def _orm_to_detail(orm: Any) -> ContentReviewDetail:
|
||||
resource = "content_review_record"
|
||||
return ContentReviewDetail(
|
||||
review_id=orm.review_id,
|
||||
channel_type=_enum(orm.channel_type, ChannelType, resource),
|
||||
channel_type=ChannelType(orm.channel_type),
|
||||
account_id=orm.account_id,
|
||||
resource_type=_enum(orm.resource_type, ContentReviewResourceType, resource),
|
||||
content_preview=orm.content_preview,
|
||||
@ -324,7 +324,7 @@ class ContentReviewRepositoryAdapter(ContentReviewRepositoryPort):
|
||||
"""
|
||||
try:
|
||||
orms = await self._repo.list(
|
||||
channel_type=filter.channel_type.value if filter.channel_type else None,
|
||||
channel_type=filter.channel_type if filter.channel_type else None,
|
||||
account_id=filter.account_id,
|
||||
verdict=filter.verdict.value if filter.verdict else None,
|
||||
start_time=_to_naive_utc(filter.start_time),
|
||||
@ -364,7 +364,7 @@ class ContentReviewRepositoryAdapter(ContentReviewRepositoryPort):
|
||||
"""
|
||||
try:
|
||||
return await self._repo.count(
|
||||
channel_type=filter.channel_type.value if filter.channel_type else None,
|
||||
channel_type=filter.channel_type if filter.channel_type else None,
|
||||
account_id=filter.account_id,
|
||||
verdict=filter.verdict.value if filter.verdict else None,
|
||||
start_time=_to_naive_utc(filter.start_time),
|
||||
@ -446,7 +446,7 @@ class ContentReviewRepositoryAdapter(ContentReviewRepositoryPort):
|
||||
data = await self._repo.query_analytics(
|
||||
start_time=_to_naive_utc(query.start_time),
|
||||
end_time=_to_naive_utc(query.end_time),
|
||||
channel_type=query.channel_type.value if query.channel_type else None,
|
||||
channel_type=query.channel_type if query.channel_type else None,
|
||||
granularity=query.granularity,
|
||||
)
|
||||
return ContentReviewAnalyticsResult(
|
||||
@ -496,7 +496,7 @@ class ContentReviewRepositoryAdapter(ContentReviewRepositoryPort):
|
||||
"""
|
||||
try:
|
||||
data = await self._repo.query_stats(
|
||||
channel_type=query.channel_type.value if query.channel_type else None,
|
||||
channel_type=query.channel_type if query.channel_type else None,
|
||||
account_id=query.account_id,
|
||||
start_time=_to_naive_utc(query.start_time),
|
||||
end_time=_to_naive_utc(query.end_time),
|
||||
|
||||
@ -366,7 +366,7 @@ class ConversationAdapter(ConversationPort):
|
||||
stmt = stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
if start_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_time)
|
||||
if end_time is not None:
|
||||
@ -425,7 +425,7 @@ class ConversationAdapter(ConversationPort):
|
||||
stmt = stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
orm = result.scalar_one_or_none()
|
||||
if orm is None:
|
||||
@ -489,7 +489,7 @@ class ConversationAdapter(ConversationPort):
|
||||
stmt = stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
if start_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_time)
|
||||
if end_time is not None:
|
||||
@ -549,7 +549,7 @@ class ConversationAdapter(ConversationPort):
|
||||
try:
|
||||
account_orm = await self._db.execute(
|
||||
select(ChannelAccountORM.id).where(
|
||||
ChannelAccountORM.channel_type == cmd.channel_type.value,
|
||||
ChannelAccountORM.channel_type == cmd.channel_type,
|
||||
ChannelAccountORM.account_id == cmd.account_id,
|
||||
ChannelAccountORM.is_deleted == 0,
|
||||
)
|
||||
@ -560,7 +560,7 @@ class ConversationAdapter(ConversationPort):
|
||||
stmt = (
|
||||
select(ChannelSessionORM)
|
||||
.where(ChannelSessionORM.peer_id == cmd.peer_id)
|
||||
.where(ChannelSessionORM.channel_type == cmd.channel_type.value)
|
||||
.where(ChannelSessionORM.channel_type == cmd.channel_type)
|
||||
.where(ChannelSessionORM.account_id == account_id_int)
|
||||
.where(ChannelSessionORM.is_deleted == 0)
|
||||
.order_by(ChannelSessionORM.created_at.desc())
|
||||
@ -587,7 +587,7 @@ class ConversationAdapter(ConversationPort):
|
||||
if not cmd.create_if_not_found:
|
||||
raise NotFoundError(
|
||||
"conversation",
|
||||
f"{cmd.peer_id}@{cmd.channel_type.value}",
|
||||
f"{cmd.peer_id}@{cmd.channel_type}",
|
||||
)
|
||||
conversation = ConversationORM(
|
||||
thread_id=str(uuid.uuid4()),
|
||||
@ -597,7 +597,7 @@ class ConversationAdapter(ConversationPort):
|
||||
status=cmd.new_conversation_status,
|
||||
extra_metadata=cmd.new_conversation_extra_metadata,
|
||||
unified_identity_id=cmd.unified_identity_id,
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
channel_account_id=cmd.account_id,
|
||||
)
|
||||
self._db.add(conversation)
|
||||
@ -1149,18 +1149,21 @@ class ConversationAdapter(ConversationPort):
|
||||
DependencyError: 数据库故障时抛出。
|
||||
"""
|
||||
try:
|
||||
# DB 时间列为 naive UTC,剥离 DTO 入参的 tzinfo(见 _to_naive_utc 说明)。
|
||||
start_naive = _to_naive_utc(start_time)
|
||||
end_naive = _to_naive_utc(end_time)
|
||||
stmt = select(MessageORM).options(selectinload(MessageORM.conversation))
|
||||
if channel_type is not None:
|
||||
stmt = stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
if role is not None:
|
||||
stmt = stmt.where(MessageORM.role == role)
|
||||
if start_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_time)
|
||||
if end_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at <= end_time)
|
||||
if start_naive is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_naive)
|
||||
if end_naive is not None:
|
||||
stmt = stmt.where(MessageORM.created_at <= end_naive)
|
||||
if channel_status is not None:
|
||||
stmt = stmt.where(MessageORM.channel_status == channel_status)
|
||||
stmt = stmt.order_by(MessageORM.created_at.desc()).limit(limit).offset(offset)
|
||||
@ -1228,6 +1231,9 @@ class ConversationAdapter(ConversationPort):
|
||||
DependencyError: 数据库故障时抛出。
|
||||
"""
|
||||
try:
|
||||
# DB 时间列为 naive UTC,剥离 DTO 入参的 tzinfo(见 _to_naive_utc 说明)。
|
||||
start_naive = _to_naive_utc(start_time)
|
||||
end_naive = _to_naive_utc(end_time)
|
||||
stmt = (
|
||||
select(func.count())
|
||||
.select_from(MessageORM)
|
||||
@ -1239,11 +1245,11 @@ class ConversationAdapter(ConversationPort):
|
||||
if role is not None:
|
||||
stmt = stmt.where(MessageORM.role == role)
|
||||
if channel_type is not None:
|
||||
stmt = stmt.where(ConversationORM.channel_type == channel_type.value)
|
||||
if start_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_time)
|
||||
if end_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at <= end_time)
|
||||
stmt = stmt.where(ConversationORM.channel_type == channel_type)
|
||||
if start_naive is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_naive)
|
||||
if end_naive is not None:
|
||||
stmt = stmt.where(MessageORM.created_at <= end_naive)
|
||||
if channel_status is not None:
|
||||
stmt = stmt.where(MessageORM.channel_status == channel_status)
|
||||
result = await self._db.execute(stmt)
|
||||
@ -1302,7 +1308,7 @@ class ConversationAdapter(ConversationPort):
|
||||
if end_naive is not None:
|
||||
stmt = stmt.where(MessageORM.created_at <= end_naive)
|
||||
if channel_type is not None:
|
||||
stmt = stmt.where(ConversationORM.channel_type == channel_type.value)
|
||||
stmt = stmt.where(ConversationORM.channel_type == channel_type)
|
||||
result = await self._db.execute(stmt)
|
||||
|
||||
by_channel: dict[str, int] = {}
|
||||
@ -1441,7 +1447,7 @@ class ConversationAdapter(ConversationPort):
|
||||
stmt = stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
if start_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_time)
|
||||
if end_time is not None:
|
||||
@ -1461,7 +1467,7 @@ class ConversationAdapter(ConversationPort):
|
||||
channel_session_id=session_id_val,
|
||||
channel_type=ChannelType(orm.conversation.channel_type)
|
||||
if orm.conversation and orm.conversation.channel_type
|
||||
else ChannelType.CUSTOM,
|
||||
else ChannelType(""),
|
||||
role=orm.role,
|
||||
content=orm.content,
|
||||
created_at=orm.created_at,
|
||||
@ -1507,7 +1513,7 @@ class ConversationAdapter(ConversationPort):
|
||||
stmt = stmt.join(
|
||||
ConversationORM,
|
||||
ConversationORM.id == MessageORM.conversation_id,
|
||||
).where(ConversationORM.channel_type == channel_type.value)
|
||||
).where(ConversationORM.channel_type == channel_type)
|
||||
if start_time is not None:
|
||||
stmt = stmt.where(MessageORM.created_at >= start_time)
|
||||
if end_time is not None:
|
||||
|
||||
@ -100,7 +100,7 @@ def orm_to_channel_account(orm: ChannelAccountORM) -> ChannelAccount:
|
||||
) from exc
|
||||
onboarding_status_raw = orm.onboarding_status or OnboardingStatus.PENDING.value
|
||||
return ChannelAccount(
|
||||
channel_type=_enum(orm.channel_type, ChannelType, "channel_account"),
|
||||
channel_type=ChannelType(orm.channel_type),
|
||||
account_id=orm.account_id,
|
||||
display_name=orm.display_name,
|
||||
config=config,
|
||||
@ -147,7 +147,7 @@ def orm_to_channel_session(orm: ChannelSessionORM) -> ChannelSession:
|
||||
"""
|
||||
return ChannelSession(
|
||||
session_id=orm.session_id,
|
||||
channel_type=_enum(orm.channel_type, ChannelType, "channel_session"),
|
||||
channel_type=ChannelType(orm.channel_type),
|
||||
account_id=str(orm.account_id),
|
||||
peer_id=orm.peer_id,
|
||||
chat_type=orm.chat_type,
|
||||
@ -191,9 +191,7 @@ def orm_to_pairing(
|
||||
channel_account_id=(account_orm.account_id if account_orm is not None else str(orm.account_id)),
|
||||
peer_id=orm.peer_id,
|
||||
status=_enum(orm.status, PairingStatus, "pairing"),
|
||||
channel_type=(
|
||||
_enum(account_orm.channel_type, ChannelType, "channel_account") if account_orm is not None else None
|
||||
),
|
||||
channel_type=(ChannelType(account_orm.channel_type) if account_orm is not None else None),
|
||||
peer_name=orm.peer_name,
|
||||
approver_id=orm.approver_id,
|
||||
approved_at=orm.approved_at,
|
||||
@ -307,7 +305,7 @@ def orm_to_user_identity(orm: ChannelUserIdentityORM) -> UserIdentity:
|
||||
user_id=str(orm.user_id) if orm.user_id is not None else None,
|
||||
identity_type=orm.identity_type,
|
||||
identity_value=orm.identity_value,
|
||||
channel_type=_enum(orm.channel_type, ChannelType, "user_identity") if orm.channel_type is not None else None,
|
||||
channel_type=ChannelType(orm.channel_type) if orm.channel_type is not None else None,
|
||||
channel_sender_id=orm.channel_sender_id,
|
||||
source=orm.source,
|
||||
created_at=orm.created_at,
|
||||
|
||||
@ -104,5 +104,5 @@ class MessageOpsPortAdapter(MessageOpsPort):
|
||||
"message_ops_adapter_not_registered",
|
||||
channel_type=str(channel_type),
|
||||
)
|
||||
raise ChannelDegradedError(channel=channel_type.value)
|
||||
raise ChannelDegradedError(channel=channel_type)
|
||||
return await adapter.recallMessage(channel_msg_id)
|
||||
|
||||
@ -336,7 +336,7 @@ class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
||||
if existing is not None:
|
||||
await self._logger.info(
|
||||
"service account already exists, returning existing",
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
uid=cmd.uid,
|
||||
)
|
||||
@ -345,13 +345,13 @@ class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
||||
|
||||
await self._logger.error(
|
||||
"service account creation failed due to integrity error",
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
raise ServiceAccountCreationError(
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
reason=f"integrity constraint violated: {exc}",
|
||||
cause=exc,
|
||||
@ -359,7 +359,7 @@ class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
||||
|
||||
except SQLAlchemyError as exc:
|
||||
raise ServiceAccountCreationError(
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
reason=f"database error: {exc}",
|
||||
cause=exc,
|
||||
@ -370,7 +370,7 @@ class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
||||
|
||||
except Exception as exc:
|
||||
raise ServiceAccountCreationError(
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
reason=f"unexpected error: {exc}",
|
||||
cause=exc,
|
||||
@ -378,7 +378,7 @@ class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
||||
|
||||
await self._logger.info(
|
||||
"service account created",
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
uid=cmd.uid,
|
||||
)
|
||||
@ -409,7 +409,7 @@ class ServiceAccountAdapter(ServiceAccountPort, ServiceAccountRepositoryPort):
|
||||
ChannelAccountORM.service_user_uid == UserORM.uid,
|
||||
)
|
||||
.where(
|
||||
ChannelAccountORM.channel_type == channel_type.value,
|
||||
ChannelAccountORM.channel_type == channel_type,
|
||||
ChannelAccountORM.account_id == account_id,
|
||||
ChannelAccountORM.is_deleted == 0,
|
||||
UserORM.user_type == "service",
|
||||
|
||||
@ -191,7 +191,7 @@ class ChannelCircuitBreaker:
|
||||
# 由上层决定阻断或放行(INV-7,不回退 read-modify-write)
|
||||
await self._logger.warn(
|
||||
"circuit_breaker half_open permits incr failed, fail-closed",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
error_type=type(exc).__name__,
|
||||
error=str(exc),
|
||||
@ -303,7 +303,7 @@ class ChannelCircuitBreaker:
|
||||
# DependencyError,由上层重试机制处理,避免错误掩盖
|
||||
await self._logger.warn(
|
||||
"circuit_breaker incr failed, fail-closed",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
error_type=type(exc).__name__,
|
||||
error=str(exc),
|
||||
@ -366,7 +366,7 @@ class ChannelCircuitBreaker:
|
||||
返回:
|
||||
缓存 key,格式为 ``circuit_breaker:{channel_type}:{account_id}``。
|
||||
"""
|
||||
return f"{self._CACHE_KEY_PREFIX}:{channel_type.value}:{account_id}"
|
||||
return f"{self._CACHE_KEY_PREFIX}:{channel_type}:{account_id}"
|
||||
|
||||
def _failureCountKey(self, channel_type: ChannelType, account_id: str) -> str:
|
||||
"""生成失败计数原子计数器缓存 key。
|
||||
@ -381,7 +381,7 @@ class ChannelCircuitBreaker:
|
||||
返回:
|
||||
缓存 key,格式为 ``circuit_breaker:{channel_type}:{account_id}:failures``。
|
||||
"""
|
||||
return f"{self._CACHE_KEY_PREFIX}:{channel_type.value}:{account_id}:failures"
|
||||
return f"{self._CACHE_KEY_PREFIX}:{channel_type}:{account_id}:failures"
|
||||
|
||||
def _halfOpenPermitsKey(self, channel_type: ChannelType, account_id: str) -> str:
|
||||
"""生成半开许可原子计数器缓存 key。
|
||||
@ -398,7 +398,7 @@ class ChannelCircuitBreaker:
|
||||
缓存 key,格式为
|
||||
``circuit_breaker:{channel_type}:{account_id}:half_open_permits``。
|
||||
"""
|
||||
return f"{self._CACHE_KEY_PREFIX}:{channel_type.value}:{account_id}:half_open_permits"
|
||||
return f"{self._CACHE_KEY_PREFIX}:{channel_type}:{account_id}:half_open_permits"
|
||||
|
||||
async def _getState(self, key: str) -> CircuitState | None:
|
||||
"""从缓存读取熔断器状态。
|
||||
@ -619,7 +619,7 @@ class ChannelCircuitBreaker:
|
||||
await self._logger.warn(
|
||||
"渠道熔断器已打开",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
reason=reason,
|
||||
)
|
||||
@ -649,7 +649,7 @@ class ChannelCircuitBreaker:
|
||||
await self._logger.info(
|
||||
"渠道熔断器已关闭(恢复)",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
)
|
||||
|
||||
|
||||
@ -256,6 +256,6 @@ class ChannelToolExecutor(ChannelToolExecutorPort):
|
||||
匹配的 ``ToolsAdapter``;未命中返回 ``None``。
|
||||
"""
|
||||
for ct, adapter in self._registry.items():
|
||||
if ct.value == channel_type:
|
||||
if ct == channel_type:
|
||||
return adapter
|
||||
return None
|
||||
|
||||
@ -337,7 +337,7 @@ class MessageOperationExecutor:
|
||||
匹配的 ``MessageOpsAdapter``;未命中返回 ``None``。
|
||||
"""
|
||||
for ct, adapter in self._registry.items():
|
||||
if ct.value == channel_type:
|
||||
if ct == channel_type:
|
||||
return adapter
|
||||
return None
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ class PluginLifecycleAuditHandler:
|
||||
target_channel: str | None = None
|
||||
plugin_manifest = self._plugin_registry.getPlugin(plugin_id)
|
||||
if plugin_manifest is not None:
|
||||
target_channel = plugin_manifest.manifest.channel_type.value
|
||||
target_channel = plugin_manifest.manifest.channel_type
|
||||
|
||||
# 构造参数摘要:携带事件原始载荷字段(version / reason / error)
|
||||
params_summary: dict[str, object] = dict(payload)
|
||||
|
||||
@ -102,7 +102,7 @@ class ChannelProbe:
|
||||
latency_ms = int((time.monotonic() - start) * 1000)
|
||||
await self._logger.warn(
|
||||
"渠道探测超时",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
timeout=str(self.PROBE_TIMEOUT),
|
||||
latency_ms=str(latency_ms),
|
||||
)
|
||||
@ -128,7 +128,7 @@ class ChannelProbe:
|
||||
except Exception as exc:
|
||||
await self._logger.warn(
|
||||
"渠道探测下游检查:DB ping 异常",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
error=str(exc),
|
||||
)
|
||||
db_ok = False
|
||||
@ -149,7 +149,7 @@ class ChannelProbe:
|
||||
except Exception as exc:
|
||||
await self._logger.warn(
|
||||
"渠道探测下游检查:Redis ping 异常",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
error=str(exc),
|
||||
)
|
||||
redis_ok = False
|
||||
@ -167,7 +167,7 @@ class ChannelProbe:
|
||||
except Exception as exc:
|
||||
await self._logger.warn(
|
||||
"渠道探测下游检查:Worker 状态查询异常",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
error=str(exc),
|
||||
)
|
||||
worker_status = None
|
||||
@ -202,7 +202,7 @@ class ChannelProbe:
|
||||
latency_ms = int((time.monotonic() - start) * 1000)
|
||||
await self._logger.warn(
|
||||
"渠道探测异常",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
error=str(e),
|
||||
latency_ms=str(latency_ms),
|
||||
)
|
||||
@ -217,7 +217,7 @@ class ChannelProbe:
|
||||
latency_ms = outcome.latency_ms if outcome.latency_ms > 0 else int((time.monotonic() - start) * 1000)
|
||||
await self._logger.info(
|
||||
"渠道探测完成",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
healthy=str(outcome.is_available),
|
||||
latency_ms=str(latency_ms),
|
||||
)
|
||||
|
||||
@ -217,7 +217,7 @@ class DiagnosticsExporter:
|
||||
"diagnostics_bundle": snapshot.diagnostics_bundle,
|
||||
"channels": [
|
||||
{
|
||||
"channel_type": ch.channel_type.value,
|
||||
"channel_type": ch.channel_type,
|
||||
"account_id": ch.account_id,
|
||||
"plugin_state": ch.plugin_state,
|
||||
"adapter_states": ch.adapter_states,
|
||||
@ -245,7 +245,7 @@ class DiagnosticsExporter:
|
||||
``{channel_type:account_id: plugin_state}`` 映射,以渠道类型与
|
||||
账户 ID 组合为键保证唯一性。
|
||||
"""
|
||||
return {f"{ch.channel_type.value}:{ch.account_id}": ch.plugin_state for ch in snapshot.channels}
|
||||
return {f"{ch.channel_type}:{ch.account_id}": ch.plugin_state for ch in snapshot.channels}
|
||||
|
||||
def _collectRecentErrors(
|
||||
self,
|
||||
@ -280,7 +280,7 @@ class DiagnosticsExporter:
|
||||
if ch.last_error:
|
||||
errors.append(
|
||||
{
|
||||
"channel_type": ch.channel_type.value,
|
||||
"channel_type": ch.channel_type,
|
||||
"account_id": ch.account_id,
|
||||
"plugin_state": ch.plugin_state,
|
||||
"error": ch.last_error,
|
||||
|
||||
@ -299,9 +299,7 @@ class HealthAggregator:
|
||||
continue
|
||||
for account_info in worker.accounts:
|
||||
if account_info.state == "error":
|
||||
degraded_components.append(
|
||||
f"transport:{account_info.channel_type}:{account_info.account_id}"
|
||||
)
|
||||
degraded_components.append(f"transport:{account_info.channel_type}:{account_info.account_id}")
|
||||
has_degraded = True
|
||||
|
||||
# 渠道账户映射:channel_type → account_id(从渠道配置读取,供填充 ChannelHealth)
|
||||
@ -355,14 +353,14 @@ class HealthAggregator:
|
||||
if self._logger is not None:
|
||||
await self._logger.warn(
|
||||
"circuit_breaker getState failed, degrade to None",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
error=str(exc),
|
||||
)
|
||||
cb_state = None
|
||||
cb_state_str = cb_state.status if cb_state is not None else None
|
||||
if cb_state is not None and cb_state.status == "open":
|
||||
degraded_components.append(f"circuit_breaker:{channel_type.value}")
|
||||
degraded_components.append(f"circuit_breaker:{channel_type}")
|
||||
has_degraded = True
|
||||
|
||||
# 适配器状态:清单声明的适配器映射为插件状态;无声明时回退至 plugin 键
|
||||
@ -536,7 +534,7 @@ class HealthAggregator:
|
||||
def _channelHealthToDict(ch: ChannelHealth) -> dict[str, Any]:
|
||||
"""将 ``ChannelHealth`` 序列化为 JSON-safe dict。"""
|
||||
return {
|
||||
"channel_type": ch.channel_type.value,
|
||||
"channel_type": ch.channel_type,
|
||||
"account_id": ch.account_id,
|
||||
"plugin_state": ch.plugin_state,
|
||||
"adapter_states": dict(ch.adapter_states),
|
||||
@ -727,7 +725,9 @@ class HealthAggregator:
|
||||
diagnostics_bundle=data.get("diagnostics_bundle"),
|
||||
worker_status=HealthAggregator._dictToWorkerStatus(data.get("worker_status")),
|
||||
redis_stream_status=HealthAggregator._dictToRedisStreamStatus(data.get("redis_stream_status")),
|
||||
db_connection_pool_status=HealthAggregator._dictToConnectionPoolStatus(data.get("db_connection_pool_status")),
|
||||
db_connection_pool_status=HealthAggregator._dictToConnectionPoolStatus(
|
||||
data.get("db_connection_pool_status")
|
||||
),
|
||||
transport_status=HealthAggregator._dictToTransportHealth(data.get("transport_status")),
|
||||
error_code=data.get("error_code", ""),
|
||||
trace_id=data.get("trace_id", ""),
|
||||
|
||||
@ -1121,11 +1121,13 @@ class PluginLifecycleManager:
|
||||
``UpdateConfigCmd`` 时传入 ``expected_version`` 实现乐观并发控制,
|
||||
避免并发加载场景下的 read-then-write 竞态。
|
||||
|
||||
键不存在与校验错误的区分:``ConfigPort.get`` 在"键不存在"与"值校验
|
||||
失败"时均抛 ``ConfigValidationError``,契约未提供直接区分手段。本方法
|
||||
利用 ``ConfigPort.getVersion`` 返回值间接判断——版本 0 表示键从未
|
||||
写入(首次加载,合法降级返回空列表);版本 > 0 表示键存在但值校验
|
||||
失败(配置损坏,fail-fast 抛 ``InternalError``,避免静默吞掉掩盖问题)。
|
||||
键不存在与校验错误的区分:``applied_migrations`` 已在
|
||||
``CONFIG_SCHEMA`` 声明 ``default=[]``,首次加载时 ``ConfigPort.get``
|
||||
返回空列表(版本 0),不再抛 ``NotFoundError``。``try-except`` 仅用于
|
||||
兜底 Redis 存值损坏(JSON 解析失败)触发的 ``ConfigValidationError``:
|
||||
通过 ``ConfigPort.getVersion`` 间接判断,版本 0 视为首次加载降级为
|
||||
空列表;版本 > 0 视为键存在但值损坏,fail-fast 抛 ``InternalError``,
|
||||
避免静默吞掉掩盖问题。
|
||||
|
||||
写入失败抛异常向上传播,触发插件加载失败处理(``_handle_failure``);
|
||||
``applied_migrations`` 影响 ``DoctorService`` 迁移检查,非观测层配置,
|
||||
@ -1140,11 +1142,12 @@ class PluginLifecycleManager:
|
||||
InternalError: ``applied_migrations`` 键存在(版本 > 0)但值校验
|
||||
失败,表明配置损坏需 fail-fast。
|
||||
"""
|
||||
target = manifest.channel_type.value
|
||||
# 读取已有迁移列表(首次加载时配置不存在,返回空列表)
|
||||
# ConfigPort.get 在"键不存在"与"校验失败"时均抛 ConfigValidationError,
|
||||
# 契约未提供直接区分手段;通过 getVersion 返回值间接判断:
|
||||
# 版本 0 → 键不存在(合法降级),版本 > 0 → 键存在但校验失败(fail-fast)。
|
||||
target = manifest.channel_type
|
||||
# 读取已有迁移列表。applied_migrations 已在 CONFIG_SCHEMA 声明
|
||||
# default=[],首次加载时 ConfigPort.get 直接返回空列表(版本 0)。
|
||||
# try-except 仅兜底 Redis 存值损坏(JSON 解析失败)触发的
|
||||
# ConfigValidationError:版本 0 视为首次加载降级,版本 > 0 视为
|
||||
# 键存在但值损坏(fail-fast)。
|
||||
existing: list[str] = []
|
||||
try:
|
||||
value = await config_port.get(
|
||||
|
||||
@ -405,13 +405,13 @@ class PluginLoader:
|
||||
message=f"清单缺少必填字段: {field_name}",
|
||||
)
|
||||
|
||||
try:
|
||||
channel_type = ChannelType(data["channel_type"])
|
||||
except ValueError as e:
|
||||
channel_type_raw = data.get("channel_type")
|
||||
if not isinstance(channel_type_raw, str) or not channel_type_raw:
|
||||
raise ValidationError(
|
||||
field="channel_type",
|
||||
message=f"未知的渠道类型: {data['channel_type']}",
|
||||
) from e
|
||||
message="channel_type must be a non-empty string",
|
||||
)
|
||||
channel_type = ChannelType(channel_type_raw)
|
||||
|
||||
try:
|
||||
failure_policy = FailurePolicy(data["failure_policy"])
|
||||
|
||||
@ -183,13 +183,13 @@ class OutboxRetryWorker:
|
||||
if self._isPluginFailed(channel_type):
|
||||
await self._publishChannelDegraded(context, channel_type, "插件处于 FAILED 状态")
|
||||
raise ChannelDegradedError(
|
||||
channel=channel_type.value,
|
||||
channel=channel_type,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
if await self._circuit_breaker.isOpen(channel_type, context.account_id):
|
||||
raise ChannelDegradedError(
|
||||
channel=channel_type.value,
|
||||
channel=channel_type,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
@ -200,7 +200,7 @@ class OutboxRetryWorker:
|
||||
await self._logger.warn(
|
||||
"outbound adapter 未注册,跳过重试",
|
||||
outbox_id=outbox_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
)
|
||||
return
|
||||
|
||||
@ -290,7 +290,7 @@ class OutboxRetryWorker:
|
||||
await self._logger.info(
|
||||
"outbox 重试成功",
|
||||
outbox_id=entry.outbox_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=context.account_id,
|
||||
channel_msg_id=channel_msg_id,
|
||||
)
|
||||
@ -334,7 +334,7 @@ class OutboxRetryWorker:
|
||||
await self._logger.warn(
|
||||
"outbox 重试失败",
|
||||
outbox_id=entry.outbox_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=context.account_id,
|
||||
retry_count=aggregate.retry_count,
|
||||
status=aggregate.status.value,
|
||||
@ -384,7 +384,7 @@ class OutboxRetryWorker:
|
||||
await self._event_publisher.publish(event.toDomainEvent())
|
||||
await self._logger.warn(
|
||||
"出站投递暂停:渠道插件降级",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=context.account_id,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
@ -230,7 +230,7 @@ class Pipeline:
|
||||
channel_format_spec = getattr(ctx, "channel_format_spec", None)
|
||||
channel_context_note = getattr(ctx, "channel_context_note", None)
|
||||
if channel_type is not None:
|
||||
fields["channel_type"] = channel_type.value
|
||||
fields["channel_type"] = channel_type
|
||||
if account_id is not None:
|
||||
fields["account_id"] = account_id
|
||||
if agent_run_id is not None:
|
||||
@ -247,7 +247,7 @@ class Pipeline:
|
||||
fields["channel_context_note"] = channel_context_note
|
||||
# 派生 channel_id,等价于 ${channel_type}:${account_id}
|
||||
if channel_type is not None and account_id is not None:
|
||||
fields["channel_id"] = f"{channel_type.value}:{account_id}"
|
||||
fields["channel_id"] = f"{channel_type}:{account_id}"
|
||||
if not ok:
|
||||
fields["error_type"] = type(err).__name__ if err else None
|
||||
try:
|
||||
|
||||
@ -87,7 +87,7 @@ class AuditContextBuilder:
|
||||
group_id / keyword),便于审计排查定位具体查询目标。
|
||||
login/* 操作:target 包含渠道类型与账户 ID,便于审计排查定位
|
||||
具体账户的登录操作。
|
||||
其他操作:优先取 ``params.target``,其次 ``target_channel.value``,
|
||||
其他操作:优先取 ``params.target``,其次 ``target_channel``,
|
||||
兜底为 operation 字符串。
|
||||
|
||||
参数:
|
||||
@ -102,7 +102,7 @@ class AuditContextBuilder:
|
||||
return self._computeDirectoryTarget(ctx)
|
||||
if ctx.operation.startswith("login/"):
|
||||
return self._computeLoginTarget(ctx)
|
||||
return ctx.params.get("target") or (ctx.target_channel.value if ctx.target_channel else ctx.operation)
|
||||
return ctx.params.get("target") or (ctx.target_channel if ctx.target_channel else ctx.operation)
|
||||
|
||||
def _computeLoginTarget(self, ctx: ControlPlaneContext) -> str:
|
||||
"""计算 login/* 操作的审计目标字符串。
|
||||
@ -116,7 +116,7 @@ class AuditContextBuilder:
|
||||
返回:
|
||||
审计目标字符串,包含渠道与账户信息。
|
||||
"""
|
||||
channel = ctx.target_channel.value if ctx.target_channel else "unknown"
|
||||
channel = ctx.target_channel if ctx.target_channel else "unknown"
|
||||
account_id = ctx.params.get("account_id", "")
|
||||
return f"{channel}:{account_id}"
|
||||
|
||||
@ -133,7 +133,7 @@ class AuditContextBuilder:
|
||||
返回:
|
||||
审计目标字符串,包含渠道、账户与查询对象信息。
|
||||
"""
|
||||
channel = ctx.target_channel.value if ctx.target_channel else "unknown"
|
||||
channel = ctx.target_channel if ctx.target_channel else "unknown"
|
||||
account_id = ctx.params.get("account_id", "")
|
||||
if ctx.operation == "directory/users":
|
||||
keyword = ctx.params.get("keyword", "")
|
||||
@ -228,7 +228,7 @@ class AuditContextBuilder:
|
||||
request_id=ctx.request_id,
|
||||
message_id=message_id,
|
||||
content_summary=content_summary,
|
||||
target_channel=ctx.target_channel.value if ctx.target_channel else None,
|
||||
target_channel=ctx.target_channel if ctx.target_channel else None,
|
||||
target_account=ctx.params.get("target_account"),
|
||||
)
|
||||
|
||||
@ -323,7 +323,7 @@ class AuditContextBuilder:
|
||||
trace_id=ctx.trace_id,
|
||||
source_ip=ctx.operator.ip,
|
||||
request_id=ctx.request_id,
|
||||
target_channel=ctx.target_channel.value if ctx.target_channel else None,
|
||||
target_channel=ctx.target_channel if ctx.target_channel else None,
|
||||
target_account=account_id,
|
||||
)
|
||||
try:
|
||||
|
||||
@ -34,7 +34,6 @@ from yuxi.channels.application.pipeline.control_plane.handlers.analytics_handler
|
||||
from yuxi.channels.application.pipeline.control_plane.handlers.audit_handler import (
|
||||
AuditHandler,
|
||||
)
|
||||
from yuxi.channels.application.pipeline.control_plane.handlers.base import BatchExecutor
|
||||
from yuxi.channels.application.pipeline.control_plane.handlers.capability_handler import (
|
||||
CapabilityHandler,
|
||||
)
|
||||
@ -347,10 +346,9 @@ class ControlPlanePipeline(Pipeline):
|
||||
logger=logger,
|
||||
masking_port=masking_port,
|
||||
)
|
||||
# 共享 Pattern D 批量编排器:PluginHandler 接收已构造的 BatchExecutor,
|
||||
# 其余批量 handler(config/account/message/session/content_review/
|
||||
# pairing/whitelist/outbox)内部按 transaction_port 自建。
|
||||
batch_executor = BatchExecutor(transaction_port)
|
||||
# 所有批量 handler(config/account/message/session/content_review/
|
||||
# pairing/whitelist/outbox/plugin)均按 transaction_port 内部自建
|
||||
# BatchExecutor,无共享实例。
|
||||
handlers = [
|
||||
ConfigHandler(
|
||||
config_manager=config_manager,
|
||||
@ -457,7 +455,11 @@ class ControlPlanePipeline(Pipeline):
|
||||
transaction_port=transaction_port,
|
||||
logger=logger,
|
||||
),
|
||||
DoctorHandler(doctor_service=doctor_service, logger=logger),
|
||||
DoctorHandler(
|
||||
doctor_service=doctor_service,
|
||||
persistence_port=persistence_port,
|
||||
logger=logger,
|
||||
),
|
||||
WebhookHandler(
|
||||
persistence_port=persistence_port,
|
||||
webhook_test_adapters=webhook_test_adapters,
|
||||
@ -468,7 +470,7 @@ class ControlPlanePipeline(Pipeline):
|
||||
plugin_loader=plugin_loader,
|
||||
plugin_registry=plugin_registry,
|
||||
config_manager=config_manager,
|
||||
batch_executor=batch_executor,
|
||||
transaction_port=transaction_port,
|
||||
logger=logger,
|
||||
),
|
||||
OutboxHandler(
|
||||
|
||||
@ -237,6 +237,16 @@ class AccountHandler(ControlPlaneHandler):
|
||||
raw_config = _requireParam(ctx, "raw_config")
|
||||
display_name = _requireParam(ctx, "display_name")
|
||||
|
||||
# 前置校验:channel_type 必须已注册生命周期适配器。未注册时
|
||||
# lifecycle service 的 _requireAdapter 会抛 NotImplementedError(501),
|
||||
# 此处提前翻译为 RuleViolationError(422),明确告知客户端该渠道
|
||||
# 类型不支持账户创建。
|
||||
if not self._lifecycle_service.hasLifecycleCapability(channel_type):
|
||||
raise RuleViolationError(
|
||||
rule=f"unsupported_channel_type:{channel_type}",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
|
||||
# 1. 解析账户 ID(失败抛 ValidationError,中止)
|
||||
account_id = await self._lifecycle_service.resolveAccountId(channel_type, raw_config, ctx.trace_id)
|
||||
|
||||
@ -735,7 +745,7 @@ class AccountHandler(ControlPlaneHandler):
|
||||
|
||||
return {
|
||||
"account_id": account_id,
|
||||
"channel_type": channel_type.value,
|
||||
"channel_type": channel_type,
|
||||
"status": account_dto.status.value,
|
||||
"enabled": account_dto.enabled,
|
||||
"circuit_breaker": _serializeCircuitState(circuit_state),
|
||||
@ -767,11 +777,11 @@ class AccountHandler(ControlPlaneHandler):
|
||||
|
||||
plugin = self.plugin_registry.getPluginByChannelType(channel_type)
|
||||
if plugin is None:
|
||||
raise NotFoundError("plugin", channel_type.value, trace_id=ctx.trace_id)
|
||||
raise NotFoundError("plugin", channel_type, trace_id=ctx.trace_id)
|
||||
lifecycle_adapter = plugin.lifecycle_adapter
|
||||
if lifecycle_adapter is None or not hasattr(lifecycle_adapter, "rotateCredentials"):
|
||||
raise NotImplementedError(
|
||||
f"rotateCredentials not supported for {channel_type.value}",
|
||||
f"rotateCredentials not supported for {channel_type}",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
|
||||
@ -824,7 +834,7 @@ class AccountHandler(ControlPlaneHandler):
|
||||
|
||||
plugin = self.plugin_registry.getPluginByChannelType(channel_type)
|
||||
if plugin is None:
|
||||
raise NotFoundError("plugin", channel_type.value, trace_id=ctx.trace_id)
|
||||
raise NotFoundError("plugin", channel_type, trace_id=ctx.trace_id)
|
||||
|
||||
start_time = time.monotonic()
|
||||
try:
|
||||
@ -899,7 +909,7 @@ class AccountHandler(ControlPlaneHandler):
|
||||
if filter_cond and "channel_type" in filter_cond:
|
||||
channel_type_value = filter_cond["channel_type"]
|
||||
elif ctx.target_channel is not None:
|
||||
channel_type_value = ctx.target_channel.value
|
||||
channel_type_value = ctx.target_channel
|
||||
if channel_type_value is None:
|
||||
raise ValidationError(
|
||||
"channel_type",
|
||||
@ -995,7 +1005,7 @@ class AccountHandler(ControlPlaneHandler):
|
||||
exported_at = utc_now()
|
||||
return {
|
||||
"account_id": account_id,
|
||||
"channel_type": channel_type.value,
|
||||
"channel_type": channel_type,
|
||||
"display_name": account_dto.display_name,
|
||||
"raw_config": raw_config,
|
||||
"exported_at": exported_at.isoformat(),
|
||||
@ -1046,7 +1056,7 @@ class AccountHandler(ControlPlaneHandler):
|
||||
)
|
||||
plugin = self.plugin_registry.getPluginByChannelType(channel_type)
|
||||
if plugin is None:
|
||||
raise NotFoundError("plugin", channel_type.value, trace_id=ctx.trace_id)
|
||||
raise NotFoundError("plugin", channel_type, trace_id=ctx.trace_id)
|
||||
if not plugin.manifest.supports_credential_cloning:
|
||||
raise RuleViolationError(
|
||||
rule="credential_cloning_not_supported",
|
||||
|
||||
@ -22,10 +22,10 @@ from yuxi.channels.application.pipeline.control_plane.handlers.base import (
|
||||
)
|
||||
from yuxi.channels.contract.errors import ValidationError
|
||||
from yuxi.channels.contract.errors.server import NotImplementedError
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.agent_management_port import (
|
||||
AgentManagementPort,
|
||||
)
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
|
||||
|
||||
class AgentHandler(ControlPlaneHandler):
|
||||
|
||||
@ -59,7 +59,7 @@ class CapabilityHandler(ControlPlaneHandler):
|
||||
plugins = self.plugin_registry.listPlugins()
|
||||
capabilities = [
|
||||
{
|
||||
"channel_type": plugin.manifest.channel_type.value,
|
||||
"channel_type": plugin.manifest.channel_type,
|
||||
"capabilities": plugin.manifest.capabilities,
|
||||
}
|
||||
for plugin in plugins
|
||||
@ -83,11 +83,11 @@ class CapabilityHandler(ControlPlaneHandler):
|
||||
if plugin is None:
|
||||
raise NotFoundError(
|
||||
resource="plugin",
|
||||
id=channel_type.value,
|
||||
id=channel_type,
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
return {
|
||||
"channel_type": channel_type.value,
|
||||
"channel_type": channel_type,
|
||||
"capabilities": plugin.manifest.capabilities,
|
||||
}
|
||||
|
||||
|
||||
@ -47,9 +47,9 @@ from yuxi.channels.contract.dtos.config import (
|
||||
UpdateConfigCmd,
|
||||
)
|
||||
from yuxi.channels.contract.errors import Error, NotFoundError, ValidationError
|
||||
from yuxi.channels.contract.policy.config_schema import HOT_RELOADABLE_KEYS
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.transaction_port import TransactionPort
|
||||
from yuxi.channels.contract.policy.config_schema import HOT_RELOADABLE_KEYS
|
||||
from yuxi.channels.core.service.config_manager import ConfigManager
|
||||
from yuxi.utils.datetime_utils import utc_now
|
||||
|
||||
@ -199,6 +199,22 @@ class ConfigHandler(ControlPlaneHandler):
|
||||
scope,
|
||||
target,
|
||||
)
|
||||
# 历史为空时区分"key 不存在"与"key 存在但无历史":
|
||||
# key 不存在(config_port.get 抛 NotFoundError)时返回 404,
|
||||
# key 存在但无历史记录时返回空列表 200。
|
||||
if not entries:
|
||||
try:
|
||||
await self.config_manager.config_port.get(
|
||||
params["key"],
|
||||
scope,
|
||||
target,
|
||||
)
|
||||
except NotFoundError:
|
||||
raise NotFoundError(
|
||||
"config",
|
||||
params["key"],
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
return {
|
||||
"key": params["key"],
|
||||
"history": [
|
||||
|
||||
@ -234,7 +234,8 @@ class DashboardHandler:
|
||||
by_channel: tuple[ChannelRealtimeStat, ...] = ()
|
||||
if self._realtime_metrics is not None:
|
||||
messages_per_second = await self._realtime_metrics.getMPS(
|
||||
query.window_seconds, query.channel_type,
|
||||
query.window_seconds,
|
||||
query.channel_type,
|
||||
)
|
||||
active_sessions = await self._realtime_metrics.getActiveSessionCount(
|
||||
query.channel_type,
|
||||
@ -246,23 +247,21 @@ class DashboardHandler:
|
||||
# 单渠道过滤:复用顶层已查询的指标,避免重复请求
|
||||
by_channel = (
|
||||
ChannelRealtimeStat(
|
||||
channel_type=query.channel_type.value,
|
||||
channel_type=query.channel_type,
|
||||
mps=messages_per_second,
|
||||
active_sessions=active_sessions,
|
||||
),
|
||||
)
|
||||
elif self._plugin_registry is not None:
|
||||
# 全局查询:遍历已注册渠道逐渠道查询,确保 by_channel 不为空
|
||||
registered_types = {
|
||||
ct for ct, _ in self._plugin_registry.listPluginAdapters()
|
||||
}
|
||||
registered_types = {ct for ct, _ in self._plugin_registry.listPluginAdapters()}
|
||||
by_channel = tuple(
|
||||
ChannelRealtimeStat(
|
||||
channel_type=ct.value,
|
||||
channel_type=ct,
|
||||
mps=await self._realtime_metrics.getMPS(query.window_seconds, ct),
|
||||
active_sessions=await self._realtime_metrics.getActiveSessionCount(ct),
|
||||
)
|
||||
for ct in sorted(registered_types, key=lambda c: c.value)
|
||||
for ct in sorted(registered_types, key=lambda c: c)
|
||||
)
|
||||
|
||||
result = RealtimeMetricsResult(
|
||||
|
||||
@ -96,7 +96,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
adapter = self.directory_adapter_registry.get(ctx.target_channel)
|
||||
if adapter is None:
|
||||
raise NotImplementedError(
|
||||
f"directory/{ctx.target_channel.value}",
|
||||
f"directory/{ctx.target_channel}",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
return adapter
|
||||
@ -116,7 +116,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
)
|
||||
if not bool(value.value):
|
||||
raise NotImplementedError(
|
||||
f"directory@{ctx.target_channel.value if ctx.target_channel else 'unknown'}",
|
||||
f"directory@{ctx.target_channel if ctx.target_channel else 'unknown'}",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
|
||||
@ -175,7 +175,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
limit=int(params.get("limit", _DEFAULT_LIST_LIMIT)),
|
||||
)
|
||||
cache_key = (
|
||||
f"directory:users:{ctx.target_channel.value if ctx.target_channel else ''}"
|
||||
f"directory:users:{ctx.target_channel if ctx.target_channel else ''}"
|
||||
f":{account_id}:search:{query.keyword or ''}:{query.cursor or ''}:{query.limit}"
|
||||
)
|
||||
|
||||
@ -209,7 +209,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
limit=int(params.get("limit", _DEFAULT_LIST_LIMIT)),
|
||||
)
|
||||
cache_key = (
|
||||
f"directory:groups:{ctx.target_channel.value if ctx.target_channel else ''}"
|
||||
f"directory:groups:{ctx.target_channel if ctx.target_channel else ''}"
|
||||
f":{account_id}:search:{query.keyword or ''}:{query.cursor or ''}:{query.limit}"
|
||||
)
|
||||
|
||||
@ -243,7 +243,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
"user_id is required for directory/user_profile",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
cache_key = f"directory:users:{ctx.target_channel.value if ctx.target_channel else ''}:{account_id}:profile:{user_id}"
|
||||
cache_key = f"directory:users:{ctx.target_channel if ctx.target_channel else ''}:{account_id}:profile:{user_id}"
|
||||
|
||||
async def _fetch() -> dict[str, Any]:
|
||||
user = await adapter.getUserProfile(user_id)
|
||||
@ -282,7 +282,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
limit=int(params.get("limit", _DEFAULT_LIST_LIMIT)),
|
||||
)
|
||||
cache_key = (
|
||||
f"directory:groups:{ctx.target_channel.value if ctx.target_channel else ''}"
|
||||
f"directory:groups:{ctx.target_channel if ctx.target_channel else ''}"
|
||||
f":{account_id}:members:{group_id}:{query.cursor or ''}:{query.limit}"
|
||||
)
|
||||
|
||||
@ -316,7 +316,9 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
"group_id is required for directory/group_detail",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
cache_key = f"directory:groups:{ctx.target_channel.value if ctx.target_channel else ''}:{account_id}:detail:{group_id}"
|
||||
cache_key = (
|
||||
f"directory:groups:{ctx.target_channel if ctx.target_channel else ''}:{account_id}:detail:{group_id}"
|
||||
)
|
||||
|
||||
async def _fetch() -> dict[str, Any]:
|
||||
group = await adapter.getGroupDetail(group_id)
|
||||
@ -360,7 +362,7 @@ class DirectoryHandler(ControlPlaneHandler):
|
||||
# 失效 CachePort 中对应的本地 TTL 缓存。
|
||||
# pattern 的 scope 段与缓存 key 第二段对齐,单一 pattern 覆盖
|
||||
# 该 scope 下所有子类型(search / profile / detail / members)。
|
||||
channel_value = ctx.target_channel.value if ctx.target_channel else ""
|
||||
channel_value = ctx.target_channel if ctx.target_channel else ""
|
||||
if self.cache_port is not None:
|
||||
if scope == "all":
|
||||
pattern = f"directory:*:{channel_value}:{account_id}:*"
|
||||
|
||||
@ -28,9 +28,10 @@ from yuxi.channels.application.pipeline.control_plane.serializers import (
|
||||
_requireAccountId,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.channel import ChannelType
|
||||
from yuxi.channels.contract.errors import ValidationError
|
||||
from yuxi.channels.contract.errors import NotFoundError, ValidationError
|
||||
from yuxi.channels.contract.errors.server import NotImplementedError
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.persistence_port import PersistencePort
|
||||
from yuxi.channels.core.service.doctor_service import DoctorService
|
||||
|
||||
#: check_id 合法格式(字母 / 数字 / 下划线 / 连字符,非空)。
|
||||
@ -77,8 +78,14 @@ class DoctorHandler:
|
||||
→ ``/repair`` → 裸路径顺序匹配,避免 ``{check_id}`` 误捕获带后缀的路径。
|
||||
"""
|
||||
|
||||
def __init__(self, doctor_service: DoctorService | None = None, logger: LoggerPort | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
doctor_service: DoctorService | None = None,
|
||||
persistence_port: PersistencePort | None = None,
|
||||
logger: LoggerPort | None = None,
|
||||
) -> None:
|
||||
self.doctor_service = doctor_service
|
||||
self.persistence_port = persistence_port
|
||||
self._logger = logger
|
||||
|
||||
def operations(
|
||||
@ -119,6 +126,10 @@ class DoctorHandler:
|
||||
"""
|
||||
channel_type = self._requireDoctorTargetChannel(ctx)
|
||||
account_id = _requireAccountId(ctx)
|
||||
# 校验账户存在性(契约要求:账户不存在抛 NotFoundError)
|
||||
account = await self.persistence_port.getChannelAccount(channel_type, account_id)
|
||||
if account is None:
|
||||
raise NotFoundError(resource="channel_account", id=account_id, trace_id=ctx.trace_id)
|
||||
return await self.doctor_service.getChecks(channel_type, account_id, trace_id=ctx.trace_id)
|
||||
|
||||
async def _doctorRun(self, ctx: ControlPlaneContext) -> dict[str, Any]:
|
||||
|
||||
@ -144,7 +144,7 @@ class HealthHandler:
|
||||
status = "disabled"
|
||||
|
||||
return {
|
||||
"channel_type": channel_type.value,
|
||||
"channel_type": channel_type,
|
||||
"account_id": account_id,
|
||||
"status": status,
|
||||
"plugin_state": plugin_state,
|
||||
@ -202,7 +202,7 @@ class HealthHandler:
|
||||
result_str = "reachable" if result.healthy else "unreachable"
|
||||
|
||||
return {
|
||||
"channel_type": channel_type.value,
|
||||
"channel_type": channel_type,
|
||||
"account_id": account_id,
|
||||
"probe_type": probe_type,
|
||||
"result": result_str,
|
||||
|
||||
@ -97,6 +97,10 @@ class LoginHandler(ControlPlaneHandler):
|
||||
raise NotImplementedError(ctx.operation, trace_id=ctx.trace_id)
|
||||
channel_type = ctx.target_channel
|
||||
account_id = _requireAccountId(ctx)
|
||||
# 校验账户存在性(契约要求:账户不存在抛 NotFoundError)
|
||||
account = await self.persistence_port.getChannelAccount(channel_type, account_id)
|
||||
if account is None:
|
||||
raise NotFoundError(resource="channel_account", id=account_id, trace_id=ctx.trace_id)
|
||||
status = await self._login_service.getLoginStatus(channel_type, account_id)
|
||||
return {"login_status": status.value}
|
||||
|
||||
|
||||
@ -211,7 +211,7 @@ class MessageHandler(ControlPlaneHandler):
|
||||
|
||||
按 ``role='admin'`` 跨会话过滤管理员发送历史,支持按渠道类型与时间
|
||||
范围进一步过滤,按 ``created_at`` 降序返回。``limit`` 默认 50、上限
|
||||
200,支持 ``offset`` 分页。返回 ``{"messages": [...], "total": int}``,
|
||||
200,支持 ``offset`` 分页。返回 ``{"items": [...], "total": int}``,
|
||||
供运营审计场景使用。
|
||||
|
||||
参数(``ctx.params``):
|
||||
@ -260,7 +260,7 @@ class MessageHandler(ControlPlaneHandler):
|
||||
end_time=end_time,
|
||||
channel_status=channel_status,
|
||||
)
|
||||
return {"messages": [_messageToDict(m, self._masking) for m in messages], "total": total}
|
||||
return {"items": [_messageToDict(m, self._masking) for m in messages], "total": total}
|
||||
|
||||
async def _messageGet(self, ctx: ControlPlaneContext) -> dict[str, Any]:
|
||||
"""执行 message/get 操作(MSG-01 消息详情用例)。
|
||||
@ -315,7 +315,7 @@ class MessageHandler(ControlPlaneHandler):
|
||||
return {
|
||||
"message_id": message.message_id,
|
||||
"conversation_id": message.conversation_id,
|
||||
"channel_type": message.channel_type.value if message.channel_type else None,
|
||||
"channel_type": message.channel_type if message.channel_type else None,
|
||||
"channel_msg_id": message.channel_msg_id,
|
||||
"channel_status": message.channel_status,
|
||||
"channel_status_history": message.channel_status_history,
|
||||
@ -370,8 +370,7 @@ class MessageHandler(ControlPlaneHandler):
|
||||
):
|
||||
raise ValidationError(
|
||||
"target_channel",
|
||||
f"target_channel {ctx.target_channel.value} does not match "
|
||||
f"message.channel_type {message.channel_type.value}",
|
||||
f"target_channel {ctx.target_channel} does not match message.channel_type {message.channel_type}",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
channel_type = ctx.target_channel or message.channel_type
|
||||
@ -504,8 +503,7 @@ class MessageHandler(ControlPlaneHandler):
|
||||
):
|
||||
raise ValidationError(
|
||||
"target_channel",
|
||||
f"target_channel {ctx.target_channel.value} does not match "
|
||||
f"message.channel_type {message.channel_type.value}",
|
||||
f"target_channel {ctx.target_channel} does not match message.channel_type {message.channel_type}",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
|
||||
|
||||
@ -446,9 +446,7 @@ class OutboxHandler(ControlPlaneHandler):
|
||||
|
||||
async def _enqueue_revived() -> None:
|
||||
for outbox_id in revived_ids_snapshot:
|
||||
await queue_port.enqueue(
|
||||
EnqueueCmd(task_name="outbox_retry", payload={"outbox_id": outbox_id})
|
||||
)
|
||||
await queue_port.enqueue(EnqueueCmd(task_name="outbox_retry", payload={"outbox_id": outbox_id}))
|
||||
|
||||
ctx.post_commit_hooks.append(_enqueue_revived)
|
||||
|
||||
|
||||
@ -66,8 +66,8 @@ from yuxi.channels.contract.errors import (
|
||||
)
|
||||
from yuxi.channels.contract.errors.server import (
|
||||
NotImplementedError,
|
||||
OperationTimeoutError,
|
||||
)
|
||||
from yuxi.channels.contract.errors.server import OperationTimeoutError
|
||||
from yuxi.channels.contract.plugin.lifecycle import LifecycleState
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.plugin_lifecycle_manager_port import (
|
||||
@ -225,7 +225,7 @@ class PluginHandler(ControlPlaneHandler):
|
||||
plugin_id=p.manifest.id,
|
||||
name=p.manifest.name,
|
||||
version=p.manifest.version,
|
||||
channel_type=p.manifest.channel_type.value,
|
||||
channel_type=p.manifest.channel_type,
|
||||
state=state.value if state else LifecycleState.UNLOADED.value,
|
||||
)
|
||||
)
|
||||
@ -248,7 +248,7 @@ class PluginHandler(ControlPlaneHandler):
|
||||
plugin_id=p.manifest.id,
|
||||
name=p.manifest.name,
|
||||
version=p.manifest.version,
|
||||
channel_type=p.manifest.channel_type.value,
|
||||
channel_type=p.manifest.channel_type,
|
||||
state=state.value if state else LifecycleState.UNLOADED.value,
|
||||
capabilities=p.manifest.capabilities,
|
||||
)
|
||||
@ -276,7 +276,7 @@ class PluginHandler(ControlPlaneHandler):
|
||||
plugin_id=manifest.manifest.id,
|
||||
name=manifest.manifest.name,
|
||||
version=manifest.manifest.version,
|
||||
channel_type=manifest.manifest.channel_type.value,
|
||||
channel_type=manifest.manifest.channel_type,
|
||||
state=state.value if state else LifecycleState.UNLOADED.value,
|
||||
manifest=manifest,
|
||||
)
|
||||
@ -609,7 +609,7 @@ class PluginHandler(ControlPlaneHandler):
|
||||
) from e
|
||||
plugins = self.plugin_registry.listPlugins()
|
||||
for pm in plugins:
|
||||
if channel_type_value is not None and pm.manifest.channel_type.value != channel_type_value:
|
||||
if channel_type_value is not None and pm.manifest.channel_type != channel_type_value:
|
||||
continue
|
||||
if target_state is not None:
|
||||
s = self.plugin_registry.getPluginState(pm.manifest.id)
|
||||
|
||||
@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
@ -51,6 +51,7 @@ from yuxi.channels.contract.ports.driven.report_repository_port import (
|
||||
ReportRepositoryPort,
|
||||
)
|
||||
from yuxi.channels.core.model.report import ReportAggregate
|
||||
from yuxi.scheduler.exceptions import SchedulerValidationError
|
||||
from yuxi.scheduler.use_cases.dto.scheduler import (
|
||||
CreateTaskInput,
|
||||
DeleteTaskInput,
|
||||
@ -104,9 +105,10 @@ class ReportsHandler(ControlPlaneHandler):
|
||||
|
||||
run_at 处理:UseCase 已将 datetime 序列化为 ISO 8601 字符串放入
|
||||
``ctx.params``(审计安全),Handler 通过 ``_coerceQueryDateTime``
|
||||
还原为 aware UTC datetime。``run_at`` 为 None 时默认当前 aware
|
||||
UTC 时间(立即执行),统一 ``.isoformat()`` 为带 ``+00:00`` 后缀
|
||||
的 ISO 字符串传给调度器。
|
||||
还原为 aware UTC datetime,再统一转换为 naive UTC 传给调度器
|
||||
(与调度器内部 ``utc_now_naive()`` 比较保持时区一致)。
|
||||
``run_at`` 为 None 时默认 naive UTC + 1s 缓冲(立即执行),
|
||||
确保 ``run_at`` 始终为未来时间以通过调度器校验。
|
||||
"""
|
||||
if self._report_repository is None or self._scheduler_service is None:
|
||||
raise NotImplementedError(ctx.operation, trace_id=ctx.trace_id)
|
||||
@ -123,9 +125,16 @@ class ReportsHandler(ControlPlaneHandler):
|
||||
)
|
||||
|
||||
if run_at_raw is None:
|
||||
run_at_dt = datetime.now(UTC)
|
||||
# 默认立即执行:使用 naive UTC + 1s 缓冲,确保通过调度器
|
||||
# "run_at 必须为未来时间" 校验(handler 与 scheduler 间存在
|
||||
# 处理时延,直接使用 now 会被 <= utc_now_naive() 判定为非未来)。
|
||||
run_at_dt = utc_now_naive() + timedelta(seconds=1)
|
||||
else:
|
||||
# 用户提供的 run_at 经 _coerceQueryDateTime 还原为 aware UTC,
|
||||
# 统一转换为 naive UTC 后传给调度器,与调度器内部
|
||||
# utc_now_naive() 比较保持时区一致(Task 6 时区修复同款)。
|
||||
run_at_dt = _coerceQueryDateTime(run_at_raw, "run_at", ctx.trace_id)
|
||||
run_at_dt = run_at_dt.astimezone(UTC).replace(tzinfo=None)
|
||||
|
||||
report_id = f"rpt_{uuid4().hex}"
|
||||
|
||||
@ -147,7 +156,16 @@ class ReportsHandler(ControlPlaneHandler):
|
||||
delete_after_run=True,
|
||||
created_by=ctx.operator.user_id,
|
||||
)
|
||||
task_output = await self._scheduler_service.create_task(task_input)
|
||||
try:
|
||||
task_output = await self._scheduler_service.create_task(task_input)
|
||||
except SchedulerValidationError as e:
|
||||
# 调度器业务规则校验失败(如 run_at 非未来时间)转为 ValidationError
|
||||
# (400),避免 SchedulerValidationError 作为非契约层异常透传为 500。
|
||||
raise ValidationError(
|
||||
field="run_at",
|
||||
message=str(e),
|
||||
trace_id=ctx.trace_id,
|
||||
) from e
|
||||
|
||||
report_with_task = Report(
|
||||
report_id=report.report_id,
|
||||
|
||||
@ -123,7 +123,20 @@ class SessionHandler(ControlPlaneHandler):
|
||||
|
||||
策略门:读取 ``merge_strategy_enabled`` 配置,falsy 时抛
|
||||
``RuleViolationError``。
|
||||
|
||||
检查顺序:先校验目标会话存在(404),再检查策略门(422),
|
||||
确保不存在的会话返回 404 而非 422。
|
||||
"""
|
||||
# 必填参数经 _requireParam 校验,缺失时抛 ValidationError(400)
|
||||
target_conversation_id = _requireParam(ctx, "target_conversation_id")
|
||||
# 目标会话存在性检查:路径 session_id 映射为 target_conversation_id,
|
||||
# 通过 conversation_id 反查渠道会话,不存在时抛 NotFoundError(404)。
|
||||
# 此检查在策略门之前,确保非存在会话返回 404 而非 422。
|
||||
target_session = await self.persistence_port.getChannelSessionByConversationId(
|
||||
target_conversation_id,
|
||||
)
|
||||
if target_session is None:
|
||||
raise NotFoundError("channel_session", target_conversation_id, trace_id=ctx.trace_id)
|
||||
# FR-07 合并策略开关:默认禁用。策略门在 usecase 层执行,
|
||||
# adapter 不再承载此业务规则(INV-8)。
|
||||
strategy_value = await self.config_manager.config_port.get("merge_strategy_enabled")
|
||||
@ -132,10 +145,9 @@ class SessionHandler(ControlPlaneHandler):
|
||||
"merge_strategy_disabled",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
# 必填参数经 _requireParam 校验,缺失时抛 ValidationError(400)
|
||||
cmd = MergeConversationCmd(
|
||||
source_conversation_id=_requireParam(ctx, "source_conversation_id"),
|
||||
target_conversation_id=_requireParam(ctx, "target_conversation_id"),
|
||||
target_conversation_id=target_conversation_id,
|
||||
operator=ctx.operator,
|
||||
reason=ctx.params.get("reason", ""),
|
||||
)
|
||||
@ -236,7 +248,7 @@ class SessionHandler(ControlPlaneHandler):
|
||||
abnormal=abnormal,
|
||||
)
|
||||
return {
|
||||
"sessions": [_channelSessionToDict(s) for s in sessions],
|
||||
"items": [_channelSessionToDict(s) for s in sessions],
|
||||
"total": total,
|
||||
}
|
||||
|
||||
|
||||
@ -86,7 +86,7 @@ class WebhookHandler(ControlPlaneHandler):
|
||||
if not accounts:
|
||||
raise NotFoundError(
|
||||
resource="channel_account",
|
||||
id=f"{channel_type.value}:<first>",
|
||||
id=f"{channel_type}:<first>",
|
||||
trace_id=ctx.trace_id,
|
||||
)
|
||||
account_dto = accounts[0]
|
||||
|
||||
@ -167,10 +167,7 @@ class WhitelistHandler(ControlPlaneHandler):
|
||||
if keyword:
|
||||
kw = keyword.lower()
|
||||
entries = tuple(
|
||||
e
|
||||
for e in entries
|
||||
if kw in e.peer_id.lower()
|
||||
or (e.peer_name is not None and kw in e.peer_name.lower())
|
||||
e for e in entries if kw in e.peer_id.lower() or (e.peer_name is not None and kw in e.peer_name.lower())
|
||||
)
|
||||
|
||||
total = len(entries)
|
||||
|
||||
@ -378,9 +378,7 @@ _OPERATION_AUDIT_META: dict[str, OperationAuditMeta] = {
|
||||
# outbox/retry 仅读 getOutboxEntry + 外部入队 enqueue,无 DB 写入。
|
||||
# db_tx_required=False 跳过 DB 事务包裹,避免无谓占用连接池;
|
||||
# 审计使用 INDEPENDENT 独立事务 best-effort(§10.1 事务边界)。
|
||||
"outbox/retry": OperationAuditMeta(
|
||||
audit_type="outbox_retry", tx_strategy="INDEPENDENT", db_tx_required=False
|
||||
),
|
||||
"outbox/retry": OperationAuditMeta(audit_type="outbox_retry", tx_strategy="INDEPENDENT", db_tx_required=False),
|
||||
"outbox/dead_letter_delete": OperationAuditMeta(audit_type="outbox_dead_letter_deleted", tx_strategy="COOPERATIVE"),
|
||||
# ---- capability ----(CAP-01~CAP-02)
|
||||
# capability 查询为只读,审计类型 admin_query;审计日志与主操作共享事务(fail-closed)。
|
||||
|
||||
@ -150,24 +150,22 @@ def _coerceQueryDateTime(
|
||||
|
||||
|
||||
def _coerceChannelType(value: Any, trace_id: str | None) -> ChannelType | None:
|
||||
"""将查询参数中的渠道类型值统一转换为 ``ChannelType`` 枚举。
|
||||
"""将查询参数中的渠道类型值统一转换为 ``ChannelType``。
|
||||
|
||||
支持 ``ChannelType`` 实例与合法枚举值字符串;``None`` 原样返回。
|
||||
非法值抛 ``ValidationError``(400),避免原生异常被 ``_executeControl``
|
||||
兜底翻译为 ``InternalError``(500)。
|
||||
支持非空字符串与 ``ChannelType`` 实例;``None`` 原样返回。
|
||||
非法值(空字符串、非字符串)抛 ``ValidationError``(400)。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, ChannelType):
|
||||
return value
|
||||
try:
|
||||
if isinstance(value, str) and value:
|
||||
return ChannelType(value)
|
||||
except ValueError as exc:
|
||||
raise ValidationError(
|
||||
"channel_type",
|
||||
f"invalid channel_type: {value!r}",
|
||||
trace_id=trace_id,
|
||||
) from exc
|
||||
raise ValidationError(
|
||||
"channel_type",
|
||||
f"invalid channel_type: {value!r}",
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
|
||||
def _coerceAccountStatus(value: Any, trace_id: str | None) -> AccountStatus | None:
|
||||
@ -299,10 +297,7 @@ def _batchUpdateConfigResultToDict(result: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"total": result.total,
|
||||
"succeeded": [{"key": item.key, "new_version": item.new_version} for item in result.succeeded],
|
||||
"failed": [
|
||||
{"id": f.id, "error_code": f.error_code, "message": f.message}
|
||||
for f in result.failed
|
||||
],
|
||||
"failed": [{"id": f.id, "error_code": f.error_code, "message": f.message} for f in result.failed],
|
||||
}
|
||||
|
||||
|
||||
@ -316,7 +311,7 @@ def _channelSessionToDict(session: Any) -> dict[str, Any]:
|
||||
"""
|
||||
return {
|
||||
"session_id": session.session_id,
|
||||
"channel_type": session.channel_type.value if session.channel_type else None,
|
||||
"channel_type": session.channel_type if session.channel_type else None,
|
||||
"account_id": session.account_id,
|
||||
"peer_id": session.peer_id,
|
||||
"chat_type": session.chat_type,
|
||||
@ -375,7 +370,7 @@ def _messageSearchItemToDict(item: MessageSearchItem) -> dict[str, Any]:
|
||||
"message_id": item.message_id,
|
||||
"conversation_id": item.conversation_id,
|
||||
"channel_session_id": item.channel_session_id,
|
||||
"channel_type": item.channel_type.value if item.channel_type else None,
|
||||
"channel_type": item.channel_type if item.channel_type else None,
|
||||
"role": item.role,
|
||||
"content": item.content,
|
||||
"snippet": item.snippet,
|
||||
@ -397,7 +392,7 @@ def _pairingRecordToDict(record: PairingRecord) -> dict[str, Any]:
|
||||
return {
|
||||
"pairing_id": record.pairing_id,
|
||||
"channel_account_id": record.channel_account_id,
|
||||
"channel_type": record.channel_type.value if record.channel_type else None,
|
||||
"channel_type": record.channel_type if record.channel_type else None,
|
||||
"peer_id": record.peer_id,
|
||||
"peer_name": record.peer_name,
|
||||
"status": record.status.value,
|
||||
@ -564,7 +559,7 @@ def _buildExportFilename(
|
||||
"""
|
||||
from datetime import datetime as _dt
|
||||
|
||||
channel = channel_type.value if hasattr(channel_type, "value") else str(channel_type)
|
||||
channel = channel_type if hasattr(channel_type, "value") else str(channel_type)
|
||||
date_str = _dt.now().strftime("%Y%m%d")
|
||||
return f"allowlist_{channel}_{account_id}_{policy_type.value}_{date_str}.csv"
|
||||
|
||||
@ -683,7 +678,7 @@ def _auditQueryToDict(query: AuditQuery) -> dict[str, Any]:
|
||||
return {
|
||||
"operation_type": query.operation_type.value if query.operation_type else None,
|
||||
"operator": query.operator,
|
||||
"target_channel": query.target_channel.value if query.target_channel else None,
|
||||
"target_channel": query.target_channel if query.target_channel else None,
|
||||
"target_account": query.target_account,
|
||||
"start_time": query.start_time.isoformat() if query.start_time else None,
|
||||
"end_time": query.end_time.isoformat() if query.end_time else None,
|
||||
|
||||
@ -326,7 +326,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
"channel context provider not registered, skip context note generation",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return run_context
|
||||
@ -341,7 +341,7 @@ class AgentRunEnqueueStage:
|
||||
)
|
||||
|
||||
plugin_context: dict[str, Any] = {
|
||||
"channel_type": context.channel_type.value,
|
||||
"channel_type": context.channel_type,
|
||||
"account_id": context.account_id,
|
||||
"peer_id": context.peer_id,
|
||||
"chat_type": context.chat_type,
|
||||
@ -368,7 +368,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
f"context note enrichment failed, skip: {exc}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
@ -418,7 +418,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
"tools adapter not registered, skip channel tools enrichment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return run_context
|
||||
@ -432,7 +432,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
f"getChannelTools failed, fallback to empty tool list: {exc}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
@ -488,7 +488,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
"message ops adapter not registered, skip message ops enrichment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return run_context
|
||||
@ -502,7 +502,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
f"getMessageOperations failed, skip message ops tools: {exc}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
@ -599,7 +599,7 @@ class AgentRunEnqueueStage:
|
||||
await self.logger.warn(
|
||||
f"config read failed, fallback to default: key={key}, default={default}, error={exc}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
|
||||
@ -146,7 +146,7 @@ class ClassifyStage:
|
||||
if session_adapter is None:
|
||||
raise ValidationError(
|
||||
"session_adapter",
|
||||
f"session adapter not registered for channel {context.channel_type.value}",
|
||||
f"session adapter not registered for channel {context.channel_type}",
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
context.peer_id = await session_adapter.getPeerId(context.raw_event)
|
||||
@ -158,7 +158,7 @@ class ClassifyStage:
|
||||
if inbound_adapter is None:
|
||||
raise ValidationError(
|
||||
"inbound_adapter",
|
||||
f"inbound adapter not registered for channel {context.channel_type.value}",
|
||||
f"inbound adapter not registered for channel {context.channel_type}",
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
context.message_content = await inbound_adapter.normalizeInbound(context.raw_event)
|
||||
@ -167,7 +167,7 @@ class ClassifyStage:
|
||||
if mention_adapter is None:
|
||||
raise ValidationError(
|
||||
"mention_adapter",
|
||||
f"mention adapter not registered for channel {context.channel_type.value}",
|
||||
f"mention adapter not registered for channel {context.channel_type}",
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
mention_context = await mention_adapter.classifyMention(context.raw_event)
|
||||
@ -181,7 +181,7 @@ class ClassifyStage:
|
||||
await self.logger.info(
|
||||
"implicit mention kind uncertain, classified as native",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
mention_context = self._mention_classifier.classify(mention_context)
|
||||
|
||||
@ -142,7 +142,7 @@ class MediaFetchStage:
|
||||
await self.logger.info(
|
||||
"attachment type not supported in v1, skip",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
attachment_type=attachment.type,
|
||||
)
|
||||
return None
|
||||
@ -175,7 +175,7 @@ class MediaFetchStage:
|
||||
await self.logger.warn(
|
||||
"adapter not implemented downloadAttachment, skip download",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return None # 附件被剔除
|
||||
@ -189,7 +189,7 @@ class MediaFetchStage:
|
||||
await self.logger.warn(
|
||||
"downloadAttachment raised exception, skip attachment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
error=str(exc),
|
||||
)
|
||||
context.degraded = True
|
||||
@ -205,7 +205,7 @@ class MediaFetchStage:
|
||||
await self.logger.warn(
|
||||
"downloadAttachment returned None, skip attachment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
)
|
||||
context.degraded = True
|
||||
return None
|
||||
@ -219,7 +219,7 @@ class MediaFetchStage:
|
||||
await self.logger.warn(
|
||||
"image size exceeds limit, skip attachment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
size=len(content),
|
||||
limit=self.MAX_ATTACHMENT_SIZE,
|
||||
)
|
||||
@ -237,7 +237,7 @@ class MediaFetchStage:
|
||||
await self.logger.warn(
|
||||
"image_processor failed, skip attachment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
error=str(exc),
|
||||
)
|
||||
context.degraded = True
|
||||
@ -252,7 +252,7 @@ class MediaFetchStage:
|
||||
await self.logger.warn(
|
||||
"image_processor returned failure, skip attachment",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
error=result.get("error", "unknown"),
|
||||
)
|
||||
context.degraded = True
|
||||
|
||||
@ -93,7 +93,7 @@ class ReceiveStage:
|
||||
plugin_state = self.plugin_registry.getPluginState(plugin_manifest.manifest.id)
|
||||
if plugin_state is LifecycleState.FAILED:
|
||||
raise ChannelDegradedError(
|
||||
context.channel_type.value,
|
||||
context.channel_type,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
return True
|
||||
|
||||
@ -200,7 +200,7 @@ class ReplyStage:
|
||||
"""
|
||||
payload_json = json.dumps(context.raw_event.payload, sort_keys=True, default=_jsonDefault)
|
||||
payload_hash = hashlib.sha256(payload_json.encode()).hexdigest()[:16]
|
||||
return f"{context.channel_type.value}:{context.account_id}:{payload_hash}"
|
||||
return f"{context.channel_type}:{context.account_id}:{payload_hash}"
|
||||
|
||||
async def _getPluginAckPolicy(self, channel_type: ChannelType) -> AckPolicy | None:
|
||||
"""查询插件声明的 ACK 策略。
|
||||
|
||||
@ -104,7 +104,7 @@ class RouteStage:
|
||||
unified_identity_id = context.unified_identity.identity_id if context.unified_identity is not None else None
|
||||
|
||||
binding_ctx = BindingContext(
|
||||
session_key=f"{context.channel_type.value}:{context.account_id}:{context.peer_id}",
|
||||
session_key=f"{context.channel_type}:{context.account_id}:{context.peer_id}",
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
chat_type=context.chat_type,
|
||||
|
||||
@ -185,7 +185,7 @@ class SecurityStage:
|
||||
"dm security decision made",
|
||||
dm_decision=context.dm_decision.value,
|
||||
pairing_id=context.pairing_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
@ -258,14 +258,14 @@ class SecurityStage:
|
||||
target=context.peer_id or "",
|
||||
result="success",
|
||||
params_summary={
|
||||
"channel_type": context.channel_type.value,
|
||||
"channel_type": context.channel_type,
|
||||
"account_id": context.account_id,
|
||||
"peer_id": context.peer_id,
|
||||
"policy": dm_policy.value,
|
||||
"decision": dm_decision.value,
|
||||
},
|
||||
trace_id=context.trace_id,
|
||||
target_channel=context.channel_type.value,
|
||||
target_channel=context.channel_type,
|
||||
target_account=context.account_id,
|
||||
)
|
||||
await self.audit_log_repository.saveAuditLog(cmd, tx=context.tx)
|
||||
|
||||
@ -88,7 +88,7 @@ class ServiceAccountResolveStage:
|
||||
)
|
||||
if service_account is None:
|
||||
raise ServiceAccountNotFoundError(
|
||||
context.channel_type.value,
|
||||
context.channel_type,
|
||||
context.account_id,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
|
||||
@ -166,7 +166,7 @@ class SessionResolveStage:
|
||||
strategy = await self._readCrossChannelIdentityStrategy(context)
|
||||
unified_identity_id, _ = self._cross_channel_association_policy.resolve(context.unified_identity, strategy)
|
||||
|
||||
lock_key = f"session:{context.channel_type.value}:{context.account_id}:{context.peer_id}"
|
||||
lock_key = f"session:{context.channel_type}:{context.account_id}:{context.peer_id}"
|
||||
lock_token = await self.cache_port.acquireAdvisoryLock(lock_key)
|
||||
try:
|
||||
session = await self.persistence_port.getChannelSessionByPeer(
|
||||
@ -232,7 +232,7 @@ class SessionResolveStage:
|
||||
"malformed cron session key, defaulting to not temporary",
|
||||
trace_id=context.trace_id,
|
||||
peer_id=context.peer_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
|
||||
@ -306,7 +306,7 @@ class SessionResolveStage:
|
||||
"cross_channel_identity_strategy read failed, fallback to isolation",
|
||||
error=str(exc),
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
|
||||
@ -99,7 +99,7 @@ class SignatureVerifyStage:
|
||||
if adapter is None:
|
||||
raise ValidationError(
|
||||
"channel_type",
|
||||
f"inbound adapter not registered for channel_type={context.channel_type.value}",
|
||||
f"inbound adapter not registered for channel_type={context.channel_type}",
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
|
||||
@ -112,7 +112,7 @@ class SignatureVerifyStage:
|
||||
await self.logger.warn(
|
||||
"signature verification skipped: adapter not implemented",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return True
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||||
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
|
||||
from yuxi.channels.core.service.capability_verifier import CapabilityVerifier
|
||||
|
||||
|
||||
__all__ = ["CapabilityVerifyStage"]
|
||||
|
||||
|
||||
|
||||
@ -34,7 +34,6 @@ from yuxi.channels.core.model.outbox_entry import OutboxEntry as OutboxAggregate
|
||||
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
|
||||
from yuxi.channels.core.service.bot_loop_budget_guard import BotLoopBudgetGuard
|
||||
|
||||
|
||||
__all__ = ["DeliverStage"]
|
||||
|
||||
|
||||
@ -176,7 +175,7 @@ class DeliverStage:
|
||||
plugin_state = self.plugin_registry.getPluginState(plugin_manifest.manifest.id)
|
||||
if plugin_state is LifecycleState.FAILED:
|
||||
raise ChannelDegradedError(
|
||||
context.channel_type.value,
|
||||
context.channel_type,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
|
||||
@ -184,7 +183,7 @@ class DeliverStage:
|
||||
if adapter is None:
|
||||
raise ValidationError(
|
||||
field="channel_type",
|
||||
message=f"outbound adapter not registered for channel type: {context.channel_type.value}",
|
||||
message=f"outbound adapter not registered for channel type: {context.channel_type}",
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
|
||||
@ -193,7 +192,7 @@ class DeliverStage:
|
||||
await self.logger.warn(
|
||||
"outbound adapter does not support outbound, skip deliver stage",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return False
|
||||
|
||||
@ -18,7 +18,6 @@ from yuxi.channels.contract.ports.driven.conversation_port import ConversationPo
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.core.service.fence_guard import FenceGuard
|
||||
|
||||
|
||||
__all__ = ["FenceCheckStage"]
|
||||
|
||||
|
||||
@ -115,7 +114,7 @@ class FenceCheckStage:
|
||||
conversation_id=context.conversation_id,
|
||||
stale_generation=verdict.stale_generation,
|
||||
current_generation=verdict.current_generation,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return False
|
||||
|
||||
@ -23,7 +23,6 @@ from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||||
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
|
||||
|
||||
__all__ = ["FormatStage"]
|
||||
|
||||
|
||||
@ -182,7 +181,7 @@ class FormatStage:
|
||||
"proof failed, degrade to fallback format "
|
||||
"(manifest-runtime inconsistency)",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
capability=cap_result.capability,
|
||||
)
|
||||
return None, None
|
||||
@ -199,7 +198,7 @@ class FormatStage:
|
||||
await self.logger.warn(
|
||||
"rich message render failed, degrade to markdown",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
error=str(exc),
|
||||
)
|
||||
context.degraded = True
|
||||
@ -214,7 +213,7 @@ class FormatStage:
|
||||
await self.logger.warn(
|
||||
"degradeToMarkdown failed, fallback to rich message text",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
error=str(degrade_exc),
|
||||
)
|
||||
context.degraded_reason = degrade_exc
|
||||
|
||||
@ -14,7 +14,6 @@ from yuxi.channels.contract.dtos.common import Attachment
|
||||
from yuxi.channels.contract.dtos.outbound import OutboundPayload, RichMessageFields
|
||||
from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||||
|
||||
|
||||
__all__ = ["LoadBuildStage"]
|
||||
|
||||
|
||||
|
||||
@ -74,7 +74,6 @@ from yuxi.channels.core.service.fence_guard import FenceGuard
|
||||
from yuxi.channels.core.service.truncation_detector import TruncationDetector
|
||||
from yuxi.channels.core.service.trusted_message_guard import TrustedMessageGuard
|
||||
|
||||
|
||||
__all__ = ["OutboundPipeline"]
|
||||
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ from yuxi.channels.contract.ports.driven.event_publisher_port import EventPublis
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.outbox_repository_port import OutboxRepositoryPort
|
||||
|
||||
|
||||
__all__ = ["OutboxMarkFailed"]
|
||||
|
||||
|
||||
|
||||
@ -37,7 +37,6 @@ from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.outbox_repository_port import OutboxRepositoryPort
|
||||
from yuxi.channels.contract.ports.driven.transaction_port import TransactionPort
|
||||
|
||||
|
||||
__all__ = ["OutboxPersistStage"]
|
||||
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ from yuxi.channels.contract.ports.driven.event_publisher_port import EventPublis
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.contract.ports.driven.outbox_repository_port import OutboxRepositoryPort
|
||||
|
||||
|
||||
__all__ = ["OutboxRollback"]
|
||||
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ from yuxi.channels.application.context.outbound_context import OutboundContext
|
||||
from yuxi.channels.contract.dtos.outbound import FinalMessage
|
||||
from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||||
|
||||
|
||||
__all__ = ["PrefixStage"]
|
||||
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ from yuxi.channels.contract.ports.driven.conversation_port import ConversationPo
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.core.service.fence_guard import FenceGuard
|
||||
|
||||
|
||||
__all__ = ["StatusWritebackStage"]
|
||||
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ from yuxi.channels.contract.ports.driven.agent_run_port import AgentRunPort
|
||||
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
|
||||
|
||||
__all__ = ["StreamChunkStage"]
|
||||
|
||||
|
||||
@ -142,7 +141,7 @@ class StreamChunkStage:
|
||||
context.delivery_mode = "persistent"
|
||||
context.degraded = True
|
||||
context.degraded_reason = ChannelDegradedError(
|
||||
context.channel_type.value,
|
||||
context.channel_type,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
return True
|
||||
@ -164,7 +163,7 @@ class StreamChunkStage:
|
||||
f"ttl={ttl_s:.2f}s), stop sending chunks "
|
||||
f"(sent={len(results)})",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
break
|
||||
@ -186,7 +185,7 @@ class StreamChunkStage:
|
||||
context.chunk_results = results
|
||||
context.delivery_mode = "persistent"
|
||||
raise ChannelDegradedError(
|
||||
context.channel_type.value,
|
||||
context.channel_type,
|
||||
trace_id=context.trace_id,
|
||||
) from exc
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ from yuxi.channels.contract.ports.driven.agent_run_port import AgentRunPort
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.core.service.truncation_detector import TruncationDetector
|
||||
|
||||
|
||||
__all__ = ["TruncationCheckStage"]
|
||||
|
||||
|
||||
@ -129,7 +128,7 @@ class TruncationCheckStage:
|
||||
f"truncation completed: original_length={original_length} completed_length={completed_length}",
|
||||
trace_id=context.trace_id,
|
||||
agent_run_id=context.agent_run_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return True
|
||||
@ -141,7 +140,7 @@ class TruncationCheckStage:
|
||||
f"original_length={original_length} completed_length={completed_length}",
|
||||
trace_id=context.trace_id,
|
||||
agent_run_id=context.agent_run_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
return True
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.ports.driven.conversation_port import ConversationPo
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
from yuxi.channels.core.service.trusted_message_guard import TrustedMessageGuard
|
||||
|
||||
|
||||
__all__ = ["TrustedInjectStage"]
|
||||
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||||
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
|
||||
|
||||
__all__ = ["TypingIndicatorStage"]
|
||||
|
||||
|
||||
@ -126,7 +125,7 @@ class TypingIndicatorStage:
|
||||
await self.logger.warn(
|
||||
"startTypingIndicator returned False, typing indicator not started",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
except Error as exc:
|
||||
@ -137,7 +136,7 @@ class TypingIndicatorStage:
|
||||
await self.logger.warn(
|
||||
f"startTypingIndicator failed: {exc!r}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
|
||||
@ -22,7 +22,6 @@ from yuxi.channels.contract.plugin.adapters.streaming_adapter import StreamingAd
|
||||
from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||||
|
||||
|
||||
__all__ = ["TypingStopStage"]
|
||||
|
||||
|
||||
@ -112,7 +111,7 @@ class TypingStopStage:
|
||||
await self.logger.warn(
|
||||
f"endStreaming failed: {exc!r}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
@ -127,7 +126,7 @@ class TypingStopStage:
|
||||
await self.logger.warn(
|
||||
"stopTypingIndicator returned False, typing indicator resource may leak",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
except Error as exc:
|
||||
@ -138,7 +137,7 @@ class TypingStopStage:
|
||||
await self.logger.warn(
|
||||
f"stopTypingIndicator failed: {exc!r}",
|
||||
trace_id=context.trace_id,
|
||||
channel_type=context.channel_type.value,
|
||||
channel_type=context.channel_type,
|
||||
account_id=context.account_id,
|
||||
)
|
||||
context.degraded = True
|
||||
|
||||
@ -15,7 +15,6 @@ from yuxi.channels.contract.errors import DmDeniedError
|
||||
from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||||
from yuxi.channels.core.registry.whitelist_registry import WhitelistRegistry
|
||||
|
||||
|
||||
__all__ = ["WhitelistCheckStage"]
|
||||
|
||||
|
||||
|
||||
@ -107,7 +107,7 @@ class BaseTransportWorker(ABC):
|
||||
|
||||
@staticmethod
|
||||
def _make_account_key(channel_type: ChannelType, account_id: str) -> str:
|
||||
return f"{channel_type.value}:{account_id}"
|
||||
return f"{channel_type}:{account_id}"
|
||||
|
||||
def _touch(self, account_key: str) -> None:
|
||||
task_info = self._tasks.get(account_key)
|
||||
@ -221,7 +221,7 @@ class BaseTransportWorker(ABC):
|
||||
if await self._circuit_breaker.isOpen(channel_type, account_id):
|
||||
await self._logger.warn(
|
||||
"circuit breaker open, skip starting account",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -234,7 +234,7 @@ class BaseTransportWorker(ABC):
|
||||
adapter=adapter,
|
||||
task=asyncio.create_task(
|
||||
self._runAccountLoopWrapper(channel_type, account_id, adapter, account_ct),
|
||||
name=f"transport-{self.transport_mode}-{channel_type.value}-{account_id}",
|
||||
name=f"transport-{self.transport_mode}-{channel_type}-{account_id}",
|
||||
),
|
||||
cancellation_token=account_ct,
|
||||
state="running",
|
||||
@ -246,7 +246,7 @@ class BaseTransportWorker(ABC):
|
||||
|
||||
await self._logger.info(
|
||||
"account transport task started",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -283,7 +283,7 @@ class BaseTransportWorker(ABC):
|
||||
|
||||
await self._logger.info(
|
||||
"account transport task stopped",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
reason=reason,
|
||||
@ -323,7 +323,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.info(
|
||||
"account transport loop started",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -334,7 +334,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.error(
|
||||
"account transport loop crashed",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(exc),
|
||||
@ -346,7 +346,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.info(
|
||||
"account transport loop ended",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -379,14 +379,14 @@ class BaseTransportWorker(ABC):
|
||||
if is_open:
|
||||
await self._logger.warn(
|
||||
"circuit breaker open, not restarting account",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
return
|
||||
await self._logger.info(
|
||||
"restarting account transport after failure",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -394,7 +394,7 @@ class BaseTransportWorker(ABC):
|
||||
except Exception as restart_exc:
|
||||
await self._logger.error(
|
||||
"failed to restart account transport",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(restart_exc),
|
||||
@ -403,7 +403,7 @@ class BaseTransportWorker(ABC):
|
||||
try:
|
||||
asyncio.create_task(
|
||||
_restart(),
|
||||
name=f"transport-restart-{self.transport_mode}-{channel_type.value}-{account_id}",
|
||||
name=f"transport-restart-{self.transport_mode}-{channel_type}-{account_id}",
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
@ -431,7 +431,7 @@ class BaseTransportWorker(ABC):
|
||||
except Exception as reset_exc:
|
||||
await self._logger.warn(
|
||||
"adapter onTransportReset failed after stall",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(reset_exc),
|
||||
@ -441,7 +441,7 @@ class BaseTransportWorker(ABC):
|
||||
if is_open:
|
||||
await self._logger.warn(
|
||||
"circuit breaker open after stall, not restarting",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -450,7 +450,7 @@ class BaseTransportWorker(ABC):
|
||||
except Exception as exc:
|
||||
await self._logger.error(
|
||||
"failed to restart account after stall",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(exc),
|
||||
@ -497,7 +497,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.error(
|
||||
"message delivery failed",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(exc),
|
||||
@ -599,7 +599,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.error(
|
||||
"failed to publish TransportErrorOccurred event",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(pub_exc),
|
||||
@ -629,7 +629,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.info(
|
||||
"published channel account offline event",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
reason=reason,
|
||||
@ -662,7 +662,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.warn(
|
||||
"transport error occurred",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
category=error.category,
|
||||
@ -681,7 +681,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.error(
|
||||
"failed to publish offline event after auth_expired",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(pub_exc),
|
||||
@ -741,7 +741,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.debug(
|
||||
"transport backoff wait",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
category=error.category,
|
||||
@ -776,7 +776,7 @@ class BaseTransportWorker(ABC):
|
||||
await self._logger.warn(
|
||||
"transport stall detected, will restart account",
|
||||
trace_id=trace_id,
|
||||
channel_type=task_info.channel_type.value,
|
||||
channel_type=task_info.channel_type,
|
||||
account_id=task_info.account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
stall_timeout_ms=self._config.stall_timeout_ms,
|
||||
@ -789,7 +789,7 @@ class BaseTransportWorker(ABC):
|
||||
task_info.account_id,
|
||||
task_info.adapter,
|
||||
),
|
||||
name=f"transport-stall-restart-{self.transport_mode}-{task_info.channel_type.value}-{task_info.account_id}",
|
||||
name=f"transport-stall-restart-{self.transport_mode}-{task_info.channel_type}-{task_info.account_id}",
|
||||
)
|
||||
|
||||
def getHealth(self) -> WorkerHealthSnapshot:
|
||||
@ -806,7 +806,7 @@ class BaseTransportWorker(ABC):
|
||||
state = ti.state if not ti.task.done() else "stopped"
|
||||
accounts.append(
|
||||
AccountHealthSnapshot(
|
||||
channel_type=ti.channel_type.value,
|
||||
channel_type=ti.channel_type,
|
||||
account_id=ti.account_id,
|
||||
state=state,
|
||||
last_activity_at=ti.last_activity_at,
|
||||
|
||||
@ -148,8 +148,8 @@ class TransportManager:
|
||||
await self._logger.info(
|
||||
"transport manager started",
|
||||
trace_id=trace_id,
|
||||
puller_channels=[ct.value for ct in puller_registry.keys()],
|
||||
stream_channels=[ct.value for ct in stream_connector_registry.keys()],
|
||||
puller_channels=[ct for ct in puller_registry.keys()],
|
||||
stream_channels=[ct for ct in stream_connector_registry.keys()],
|
||||
)
|
||||
|
||||
async def stop(self, timeout: float = 5.0) -> None:
|
||||
@ -326,7 +326,7 @@ class TransportManager:
|
||||
await self._logger.info(
|
||||
"received account online event, starting transport",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
)
|
||||
|
||||
@ -365,7 +365,7 @@ class TransportManager:
|
||||
await self._logger.info(
|
||||
"received account offline event, stopping transport",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
@ -149,7 +149,7 @@ class PullerWorker(BaseTransportWorker):
|
||||
await self._logger.error(
|
||||
"failed to deliver inbound message",
|
||||
trace_id=error_trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(e),
|
||||
@ -172,7 +172,7 @@ class PullerWorker(BaseTransportWorker):
|
||||
await self._logger.warn(
|
||||
"failed to persist transport cursor",
|
||||
trace_id=error_trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
cursor=cursor,
|
||||
error=str(cursor_exc),
|
||||
|
||||
@ -104,7 +104,7 @@ class StreamWorker(BaseTransportWorker):
|
||||
await self._logger.info(
|
||||
"stream connection established",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
)
|
||||
@ -146,7 +146,7 @@ class StreamWorker(BaseTransportWorker):
|
||||
heartbeat_interval_s,
|
||||
cancellation_token,
|
||||
),
|
||||
name=f"stream-heartbeat-{channel_type.value}-{account_id}",
|
||||
name=f"stream-heartbeat-{channel_type}-{account_id}",
|
||||
)
|
||||
|
||||
# 3. 接收循环
|
||||
@ -223,7 +223,7 @@ class StreamWorker(BaseTransportWorker):
|
||||
await self._logger.warn(
|
||||
"stream receive error, will reconnect",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(exc),
|
||||
@ -238,7 +238,7 @@ class StreamWorker(BaseTransportWorker):
|
||||
await self._logger.error(
|
||||
"failed to deliver stream message",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(exc),
|
||||
@ -277,7 +277,7 @@ class StreamWorker(BaseTransportWorker):
|
||||
await self._logger.warn(
|
||||
"stream heartbeat failed",
|
||||
trace_id=trace_id,
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
transport_mode=self.transport_mode,
|
||||
error=str(exc),
|
||||
|
||||
@ -42,6 +42,7 @@ from yuxi.channels.contract.errors import (
|
||||
DependencyError,
|
||||
DmDeniedError,
|
||||
Error,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
)
|
||||
@ -292,7 +293,7 @@ class AdminMessageService:
|
||||
for target in self._parseTargets(cmd.target):
|
||||
channel_type, _, _ = self._resolveTarget(target)
|
||||
if channel_type is not None:
|
||||
target_channel = channel_type.value
|
||||
target_channel = str(channel_type)
|
||||
break
|
||||
|
||||
has_failures = bool(result.failures)
|
||||
@ -432,7 +433,7 @@ class AdminMessageService:
|
||||
error_code="VALIDATION_ERROR",
|
||||
message=(
|
||||
f"message length {len(cmd.content.text)} exceeds max "
|
||||
f"{max_length} for channel {channel_type.value}"
|
||||
f"{max_length} for channel {channel_type}"
|
||||
),
|
||||
retryable=False,
|
||||
)
|
||||
@ -449,6 +450,8 @@ class AdminMessageService:
|
||||
policy,
|
||||
target_type=cmd.target_type,
|
||||
)
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
await self._logger.error(
|
||||
f"session create failed for target {target}: {e}",
|
||||
@ -648,7 +651,8 @@ class AdminMessageService:
|
||||
|
||||
支持格式:
|
||||
- ``channel_type:account_id:session_id``:完整路由信息
|
||||
- ``session_id``:仅会话 ID,channel_type 置为 CUSTOM、account_id 置空
|
||||
- ``session_id``:仅会话 ID,channel_type 与 account_id 置空,
|
||||
视为格式非法(返回 None)
|
||||
|
||||
``split(":", 2)`` 限制分割次数为 2,确保 ``session_id`` 中含冒号
|
||||
时不会被截断(部分渠道的会话 ID 含 ``:`` 分隔符)。
|
||||
@ -658,18 +662,16 @@ class AdminMessageService:
|
||||
|
||||
返回:
|
||||
元组 (channel_type, account_id, session_id)。channel_type 为
|
||||
None 表示目标格式非法(channel_type 不是合法的渠道类型)。
|
||||
None 表示目标格式非法(缺少 channel_type 段或 channel_type 段为空)。
|
||||
"""
|
||||
parts = target.split(":", 2)
|
||||
if len(parts) < 3:
|
||||
return ChannelType.CUSTOM, "", target
|
||||
return None, "", target
|
||||
|
||||
channel_type_str, account_id, session_id = parts[0], parts[1], parts[2]
|
||||
try:
|
||||
channel_type = ChannelType(channel_type_str)
|
||||
except ValueError:
|
||||
if not channel_type_str:
|
||||
return None, "", ""
|
||||
return channel_type, account_id, session_id
|
||||
return ChannelType(channel_type_str), account_id, session_id
|
||||
|
||||
def _toFailureDetail(self, target: str, err: Error | None) -> FailureDetail:
|
||||
"""将管道错误转换为失败详情。
|
||||
|
||||
@ -123,8 +123,8 @@ from yuxi.channels.contract.errors import (
|
||||
Error,
|
||||
InternalError,
|
||||
NotFoundError,
|
||||
RuleViolationError,
|
||||
OperationTimeoutError,
|
||||
RuleViolationError,
|
||||
ValidationError,
|
||||
)
|
||||
from yuxi.channels.contract.ports.driven.channel_session_repository_port import (
|
||||
@ -357,9 +357,7 @@ class ChannelControlService:
|
||||
)
|
||||
# 批量操作部分成功时返回 partial 状态,供 raiseOnControlFailure
|
||||
# 放行并由 router 在响应 envelope 中准确反映部分成功语义。
|
||||
status: Literal["success", "partial"] = (
|
||||
"partial" if ctx.dispatch_partial else "success"
|
||||
)
|
||||
status: Literal["success", "partial"] = "partial" if ctx.dispatch_partial else "success"
|
||||
return ControlResult(
|
||||
status=status,
|
||||
data=ctx.dispatch_result,
|
||||
@ -552,11 +550,7 @@ class ChannelControlService:
|
||||
``doctor/checks/{id}/repair/preview``,与 ``confirmed=True``(写操作)
|
||||
的 ``doctor/checks/{id}/repair`` 拆分,确保只读预览不被审计为写操作。
|
||||
"""
|
||||
operation = (
|
||||
f"doctor/checks/{check_id}/repair/preview"
|
||||
if not confirmed
|
||||
else f"doctor/checks/{check_id}/repair"
|
||||
)
|
||||
operation = f"doctor/checks/{check_id}/repair/preview" if not confirmed else f"doctor/checks/{check_id}/repair"
|
||||
cmd = ControlCmd(
|
||||
operation=operation,
|
||||
operator=operator,
|
||||
@ -1111,9 +1105,9 @@ class ChannelControlService:
|
||||
"""
|
||||
params: dict[str, Any] = {"granularity": query.granularity}
|
||||
if query.start_time is not None:
|
||||
params["start_time"] = query.start_time
|
||||
params["start_time"] = query.start_time.isoformat()
|
||||
if query.end_time is not None:
|
||||
params["end_time"] = query.end_time
|
||||
params["end_time"] = query.end_time.isoformat()
|
||||
cmd = ControlCmd(
|
||||
operation="pairing/stats",
|
||||
operator=operator,
|
||||
@ -1754,9 +1748,9 @@ class ChannelControlService:
|
||||
if query.target_account is not None:
|
||||
params["target_account"] = query.target_account
|
||||
if query.start_time is not None:
|
||||
params["start_time"] = query.start_time
|
||||
params["start_time"] = query.start_time.isoformat()
|
||||
if query.end_time is not None:
|
||||
params["end_time"] = query.end_time
|
||||
params["end_time"] = query.end_time.isoformat()
|
||||
if query.trace_id is not None:
|
||||
params["trace_id"] = query.trace_id
|
||||
cmd = ControlCmd(
|
||||
@ -1779,9 +1773,9 @@ class ChannelControlService:
|
||||
if query.target_account is not None:
|
||||
params["target_account"] = query.target_account
|
||||
if query.start_time is not None:
|
||||
params["start_time"] = query.start_time
|
||||
params["start_time"] = query.start_time.isoformat()
|
||||
if query.end_time is not None:
|
||||
params["end_time"] = query.end_time
|
||||
params["end_time"] = query.end_time.isoformat()
|
||||
cmd = ControlCmd(
|
||||
operation="audit/stats",
|
||||
operator=operator,
|
||||
@ -1813,9 +1807,9 @@ class ChannelControlService:
|
||||
if query.target_account is not None:
|
||||
params["target_account"] = query.target_account
|
||||
if query.start_time is not None:
|
||||
params["start_time"] = query.start_time
|
||||
params["start_time"] = query.start_time.isoformat()
|
||||
if query.end_time is not None:
|
||||
params["end_time"] = query.end_time
|
||||
params["end_time"] = query.end_time.isoformat()
|
||||
if query.trace_id is not None:
|
||||
params["trace_id"] = query.trace_id
|
||||
cmd = ControlCmd(
|
||||
@ -2418,7 +2412,7 @@ class ChannelControlService:
|
||||
"filter_role": "admin",
|
||||
}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
if start_time is not None:
|
||||
params["start_time"] = start_time.isoformat()
|
||||
if end_time is not None:
|
||||
@ -2505,7 +2499,7 @@ class ChannelControlService:
|
||||
"""全文搜索消息(MSG-SEARCH-01)。"""
|
||||
params: dict[str, Any] = {"keyword": keyword, "limit": limit, "offset": offset}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
if channel_session_id is not None:
|
||||
params["channel_session_id"] = channel_session_id
|
||||
if peer_id is not None:
|
||||
@ -2914,7 +2908,7 @@ class ChannelControlService:
|
||||
"""获取 outbox 统计信息(OBX-02)。"""
|
||||
params: dict[str, Any] = {}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
cmd = ControlCmd(
|
||||
operation="outbox/stats",
|
||||
operator=operator,
|
||||
@ -3036,7 +3030,7 @@ class ChannelControlService:
|
||||
"""
|
||||
params: dict[str, Any] = {"format": cmd.format, "limit": cmd.limit}
|
||||
if cmd.channel_type is not None:
|
||||
params["channel_type"] = cmd.channel_type.value
|
||||
params["channel_type"] = cmd.channel_type
|
||||
if cmd.created_after is not None:
|
||||
params["created_after"] = cmd.created_after
|
||||
if cmd.created_before is not None:
|
||||
@ -3067,13 +3061,13 @@ class ChannelControlService:
|
||||
operator: 操作人(审计用)。
|
||||
"""
|
||||
params: dict[str, Any] = {
|
||||
"start_time": query.start_time,
|
||||
"end_time": query.end_time,
|
||||
"start_time": query.start_time.isoformat() if query.start_time else None,
|
||||
"end_time": query.end_time.isoformat() if query.end_time else None,
|
||||
"granularity": query.granularity,
|
||||
"metric": query.metric,
|
||||
}
|
||||
if query.channel_type is not None:
|
||||
params["channel_type"] = query.channel_type.value
|
||||
params["channel_type"] = query.channel_type
|
||||
cmd = ControlCmd(
|
||||
operation="outbox/trend",
|
||||
operator=operator,
|
||||
@ -3128,7 +3122,7 @@ class ChannelControlService:
|
||||
构造 ``operation="content_review/preview"`` 的 ``ControlCmd``,委托
|
||||
``_executeControl`` 走控制面管道(auth → permission → rate_limit →
|
||||
dispatch → audit)。``params["target"]`` 显式设置为
|
||||
``f"{channel_type.value}:{account_id}"``,供
|
||||
``f"{channel_type}:{account_id}"``,供
|
||||
``AuditContextBuilder._computeTarget`` 读取,确保审计日志的
|
||||
``target`` 字段携带账户上下文(与 ``directory/*`` / ``login/*`` 的
|
||||
多段 target 风格一致)。
|
||||
@ -3148,7 +3142,7 @@ class ChannelControlService:
|
||||
"peer_id": peer_id,
|
||||
# AuditContextBuilder._computeTarget 读取 params["target"]
|
||||
# 构造 target=f"{channel_type}:{account_id}"
|
||||
"target": f"{channel_type.value}:{account_id}",
|
||||
"target": f"{channel_type}:{account_id}",
|
||||
},
|
||||
)
|
||||
return await self._executeControl(cmd)
|
||||
@ -3170,9 +3164,9 @@ class ChannelControlService:
|
||||
if query.account_id is not None:
|
||||
params["account_id"] = query.account_id
|
||||
if query.start_time is not None:
|
||||
params["start_time"] = query.start_time
|
||||
params["start_time"] = query.start_time.isoformat()
|
||||
if query.end_time is not None:
|
||||
params["end_time"] = query.end_time
|
||||
params["end_time"] = query.end_time.isoformat()
|
||||
cmd = ControlCmd(
|
||||
operation="content_review/stats",
|
||||
operator=operator,
|
||||
@ -3234,7 +3228,7 @@ class ChannelControlService:
|
||||
"""查询账户总览(DSB-02)。"""
|
||||
params: dict[str, Any] = {}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
return await self._executeControl(
|
||||
ControlCmd(
|
||||
operation="dashboard/accounts",
|
||||
@ -3254,7 +3248,7 @@ class ChannelControlService:
|
||||
"""查询消息总览(DSB-03)。"""
|
||||
params: dict[str, Any] = {}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
if start_time is not None:
|
||||
params["start_time"] = start_time
|
||||
if end_time is not None:
|
||||
@ -3276,7 +3270,7 @@ class ChannelControlService:
|
||||
"""查询会话总览(DSB-04)。"""
|
||||
params: dict[str, Any] = {}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
return await self._executeControl(
|
||||
ControlCmd(
|
||||
operation="dashboard/sessions",
|
||||
@ -3296,7 +3290,7 @@ class ChannelControlService:
|
||||
"""查询投递总览(DSB-DELIVERY)。"""
|
||||
params: dict[str, Any] = {}
|
||||
if channel_type is not None:
|
||||
params["channel_type"] = channel_type.value
|
||||
params["channel_type"] = channel_type
|
||||
if start_time is not None:
|
||||
params["start_time"] = start_time
|
||||
if end_time is not None:
|
||||
@ -3318,7 +3312,7 @@ class ChannelControlService:
|
||||
"""查询实时监控指标(DSB-REALTIME)。"""
|
||||
params: dict[str, Any] = {"window_seconds": query.window_seconds}
|
||||
if query.channel_type is not None:
|
||||
params["channel_type"] = query.channel_type.value
|
||||
params["channel_type"] = query.channel_type
|
||||
return await self._executeControl(
|
||||
ControlCmd(
|
||||
operation="dashboard/realtime",
|
||||
|
||||
@ -29,8 +29,8 @@ from yuxi.channels.contract.errors import (
|
||||
Error,
|
||||
InternalError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
OperationTimeoutError,
|
||||
RateLimitError,
|
||||
)
|
||||
from yuxi.channels.contract.ports.driven.audit_log_repository_port import (
|
||||
AuditLogRepositoryPort,
|
||||
@ -132,7 +132,7 @@ class HealthCheckService:
|
||||
except Error as e:
|
||||
await self._logger.error(
|
||||
f"health check failed: {e}",
|
||||
channel_filter=(query.channel_filter.value if query.channel_filter is not None else None),
|
||||
channel_filter=(query.channel_filter if query.channel_filter is not None else None),
|
||||
)
|
||||
return HealthSnapshot(
|
||||
status="unhealthy",
|
||||
@ -285,7 +285,7 @@ class HealthCheckService:
|
||||
if account is None or not account.enabled:
|
||||
raise NotFoundError(
|
||||
resource="channel_account",
|
||||
id=f"{channel_type.value}:{account_id}",
|
||||
id=f"{channel_type}:{account_id}",
|
||||
trace_id=trace_id or operator.request_id,
|
||||
)
|
||||
|
||||
@ -313,10 +313,10 @@ class HealthCheckService:
|
||||
cmd = SaveAuditLogCmd(
|
||||
operator=operator.user_id,
|
||||
operation=AuditOperationType.CHANNEL_PROBED.value,
|
||||
target=f"{channel_type.value}:{account_id}",
|
||||
target=f"{channel_type}:{account_id}",
|
||||
result="success" if result.healthy else "failed",
|
||||
params_summary={
|
||||
"channel_type": channel_type.value,
|
||||
"channel_type": channel_type,
|
||||
"account_id": account_id,
|
||||
"healthy": result.healthy,
|
||||
"latency_ms": result.latency_ms,
|
||||
@ -325,7 +325,7 @@ class HealthCheckService:
|
||||
trace_id=trace_id or operator.request_id,
|
||||
source_ip=operator.ip,
|
||||
request_id=operator.request_id,
|
||||
target_channel=channel_type.value,
|
||||
target_channel=channel_type,
|
||||
target_account=account_id,
|
||||
)
|
||||
try:
|
||||
@ -335,7 +335,7 @@ class HealthCheckService:
|
||||
# (探测已执行,副作用已产生,审计失败不应丢弃探测结果)。
|
||||
await self._logger.warn(
|
||||
"probe audit log write failed",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
error=str(exc),
|
||||
)
|
||||
@ -358,7 +358,7 @@ class HealthCheckService:
|
||||
await self._logger.info(
|
||||
"diagnostics exported",
|
||||
operator=request.operator.user_id,
|
||||
channel_filter=(request.channel_filter.value if request.channel_filter is not None else None),
|
||||
channel_filter=(request.channel_filter if request.channel_filter is not None else None),
|
||||
include_audit_logs=request.include_audit_logs,
|
||||
include_error_logs=request.include_error_logs,
|
||||
)
|
||||
|
||||
@ -53,8 +53,8 @@ from yuxi.channels.contract.errors import (
|
||||
InternalError,
|
||||
NotFoundError,
|
||||
NotImplementedError,
|
||||
OperationTimeoutError,
|
||||
)
|
||||
from yuxi.channels.contract.errors import OperationTimeoutError
|
||||
from yuxi.channels.contract.plugin.adapters.inbound_adapter import InboundAdapter
|
||||
from yuxi.channels.contract.ports.driven.channel_account_repository_port import (
|
||||
ChannelAccountRepositoryPort,
|
||||
@ -137,7 +137,8 @@ class InboundMessageService:
|
||||
兜底任务。
|
||||
inbound_adapter_registry: 入站适配器注册表,按渠道类型索引,
|
||||
供 ``receiveWebhook`` 校验渠道适配器是否注册(WHK-RECV-01)。
|
||||
可选,未提供时 ``receiveWebhook`` 抛 ``NotFoundError``。
|
||||
可选,未提供时 ``receiveWebhook`` 抛 ``NotImplementedError``;
|
||||
渠道类型未在注册表中时抛 ``NotFoundError``。
|
||||
"""
|
||||
self._inbound_pipeline = inbound_pipeline
|
||||
self._outbound_pipeline = outbound_pipeline
|
||||
@ -238,7 +239,7 @@ class InboundMessageService:
|
||||
"inbound pipeline crashed",
|
||||
trace_id=trace_id,
|
||||
exc_info=e,
|
||||
channel_type=cmd.channel_type.value,
|
||||
channel_type=cmd.channel_type,
|
||||
account_id=cmd.account_id,
|
||||
)
|
||||
return InboundResult(
|
||||
@ -280,9 +281,10 @@ class InboundMessageService:
|
||||
复用入站管道能力。
|
||||
|
||||
处理步骤:
|
||||
1. 从 ``inbound_adapter_registry`` 取渠道适配器,未注册抛
|
||||
``NotImplementedError("receive_webhook")``(HTTP 501),表示
|
||||
该渠道未实现入站能力。
|
||||
1. 从 ``inbound_adapter_registry`` 取渠道适配器:注册表未注入抛
|
||||
``NotImplementedError("receive_webhook")``(HTTP 501),表示入站
|
||||
能力未启用;渠道类型未在注册表中抛 ``NotFoundError``(HTTP 404),
|
||||
表示该渠道未注册入站适配器。
|
||||
2. 构造 ``RawEvent`` 后调用 ``adapter.extractWebhookResponse`` 检查
|
||||
是否为 Webhook 配置验证请求(如飞书 challenge 握手)。验证请求
|
||||
短路返回 ``InboundResult(raw_response=...)``,不进入入站管道,
|
||||
@ -306,21 +308,23 @@ class InboundMessageService:
|
||||
Router 直接作为 HTTP 响应体返回。
|
||||
|
||||
抛出:
|
||||
NotImplementedError: 渠道适配器未注册(registry 为 None 或渠道类型
|
||||
未在注册表中),HTTP 501。
|
||||
NotFoundError: 该渠道类型无启用账户。
|
||||
NotImplementedError: 入站适配器注册表未注入(registry 为 None),
|
||||
HTTP 501,表示入站能力未启用。
|
||||
NotFoundError: 渠道类型未在注册表中(adapter 为 None)或该渠道
|
||||
类型无启用账户,HTTP 404。
|
||||
"""
|
||||
if self._inbound_adapter_registry is None:
|
||||
raise NotImplementedError(
|
||||
operation="receive_webhook",
|
||||
message=f"inbound adapter not registered for channel: {cmd.channel_type.value}",
|
||||
message=f"inbound adapter not registered for channel: {cmd.channel_type}",
|
||||
)
|
||||
|
||||
adapter = self._inbound_adapter_registry.get(cmd.channel_type)
|
||||
if adapter is None:
|
||||
raise NotImplementedError(
|
||||
operation="receive_webhook",
|
||||
message=f"inbound adapter not registered for channel: {cmd.channel_type.value}",
|
||||
raise NotFoundError(
|
||||
"inbound_adapter",
|
||||
f"{cmd.channel_type}",
|
||||
trace_id=cmd.trace_id,
|
||||
)
|
||||
|
||||
raw_event = await self._buildRawEvent(cmd.raw_event, cmd.headers, "webhook", cmd.channel_type)
|
||||
@ -346,7 +350,7 @@ class InboundMessageService:
|
||||
if not accounts:
|
||||
raise NotFoundError(
|
||||
"channel_account",
|
||||
f"{cmd.channel_type.value}:<webhook>",
|
||||
f"{cmd.channel_type}:<webhook>",
|
||||
)
|
||||
account_id = accounts[0].account_id
|
||||
|
||||
@ -383,23 +387,24 @@ class InboundMessageService:
|
||||
``valid=False`` 时 ``reason`` 填充失败原因。
|
||||
|
||||
抛出:
|
||||
NotImplementedError: 渠道适配器未注册(registry 为 None 或渠道类型
|
||||
未在注册表中),HTTP 501,表示该渠道未实现入站签名校验能力。
|
||||
NotImplementedError: 入站适配器注册表未注入(registry 为 None),
|
||||
HTTP 501,表示入站签名校验能力未启用。
|
||||
NotFoundError: 渠道类型未在注册表中(adapter 为 None),HTTP 404。
|
||||
"""
|
||||
if self._inbound_adapter_registry is None:
|
||||
raise NotImplementedError(
|
||||
operation="verify_signature",
|
||||
message=f"inbound adapter not registered for channel: {channel_type.value}",
|
||||
message=f"inbound adapter not registered for channel: {channel_type}",
|
||||
)
|
||||
|
||||
adapter = self._inbound_adapter_registry.get(channel_type)
|
||||
if adapter is None:
|
||||
raise NotImplementedError(
|
||||
operation="verify_signature",
|
||||
message=f"inbound adapter not registered for channel: {channel_type.value}",
|
||||
raise NotFoundError(
|
||||
"inbound_adapter",
|
||||
f"{channel_type}",
|
||||
)
|
||||
|
||||
raw_event_obj = await self._buildRawEvent(raw_event, headers, "webhook_verify", channel_type)
|
||||
raw_event_obj = await self._buildRawEvent(raw_event, headers, "webhook", channel_type)
|
||||
return await adapter.verifySignature(raw_event_obj)
|
||||
|
||||
async def _buildRawEvent(
|
||||
@ -421,7 +426,7 @@ class InboundMessageService:
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
await self._logger.debug(
|
||||
"webhook raw_event is not valid JSON, wrapping as raw_body",
|
||||
channel_type=channel_type.value,
|
||||
channel_type=channel_type,
|
||||
error=str(e),
|
||||
)
|
||||
payload = {"raw_body": raw_event}
|
||||
@ -492,7 +497,7 @@ class InboundMessageService:
|
||||
"outbound pipeline crashed during agent response delivery",
|
||||
trace_id=ctx.trace_id,
|
||||
agent_run_id=ctx.agent_run_id,
|
||||
channel_type=ctx.channel_type.value,
|
||||
channel_type=ctx.channel_type,
|
||||
account_id=ctx.account_id,
|
||||
exc_info=e,
|
||||
)
|
||||
@ -505,7 +510,7 @@ class InboundMessageService:
|
||||
f"outbound pipeline failed during agent response delivery: {err.message}",
|
||||
trace_id=ctx.trace_id,
|
||||
agent_run_id=ctx.agent_run_id,
|
||||
channel_type=ctx.channel_type.value,
|
||||
channel_type=ctx.channel_type,
|
||||
account_id=ctx.account_id,
|
||||
)
|
||||
self._handleOutboundFailure(ctx, err.message)
|
||||
@ -569,7 +574,7 @@ class InboundMessageService:
|
||||
await self._logger.exception(
|
||||
"outbound pipeline crashed during silent command response delivery",
|
||||
trace_id=ctx.trace_id,
|
||||
channel_type=ctx.channel_type.value,
|
||||
channel_type=ctx.channel_type,
|
||||
account_id=ctx.account_id,
|
||||
exc_info=e,
|
||||
)
|
||||
@ -581,7 +586,7 @@ class InboundMessageService:
|
||||
await self._logger.error(
|
||||
f"outbound pipeline failed during silent command response delivery: {err.message}",
|
||||
trace_id=ctx.trace_id,
|
||||
channel_type=ctx.channel_type.value,
|
||||
channel_type=ctx.channel_type,
|
||||
account_id=ctx.account_id,
|
||||
)
|
||||
self._handleOutboundFailure(ctx, err.message)
|
||||
|
||||
@ -70,6 +70,7 @@ from yuxi.channels.contract.errors import (
|
||||
Error,
|
||||
InternalError,
|
||||
NotFoundError,
|
||||
OperationTimeoutError,
|
||||
PairingExpiredError,
|
||||
PermissionDeniedError,
|
||||
PluginAlreadyRegisteredError,
|
||||
@ -79,7 +80,6 @@ from yuxi.channels.contract.errors import (
|
||||
RuleViolationError,
|
||||
SchemaInitializationError,
|
||||
ServerError,
|
||||
OperationTimeoutError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
@ -233,6 +233,7 @@ from yuxi.channels.contract.dtos.lifecycle import (
|
||||
LifecycleCmd,
|
||||
LifecycleResult,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.logger import LogLevel
|
||||
from yuxi.channels.contract.dtos.login import (
|
||||
ForceLogoutCmd,
|
||||
ForceLogoutResult,
|
||||
@ -240,7 +241,6 @@ from yuxi.channels.contract.dtos.login import (
|
||||
QrLoginStartResult,
|
||||
QrLoginWaitResult,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.logger import LogLevel
|
||||
from yuxi.channels.contract.dtos.media import (
|
||||
MediaDownloadResult,
|
||||
MediaMetadata,
|
||||
@ -342,6 +342,10 @@ from yuxi.channels.contract.dtos.route import (
|
||||
NestedWhitelistDecision,
|
||||
RouteBinding,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.service_account import (
|
||||
CreateServiceAccountCmd,
|
||||
ServiceAccount,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.session import (
|
||||
ChannelSessionId,
|
||||
ChatType,
|
||||
@ -350,24 +354,12 @@ from yuxi.channels.contract.dtos.session import (
|
||||
SessionOwner,
|
||||
TemporarySessionPattern,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.service_account import (
|
||||
CreateServiceAccountCmd,
|
||||
ServiceAccount,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.status import (
|
||||
EventType,
|
||||
MessageStatus,
|
||||
StatusPayload,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.stream_event import StreamEvent
|
||||
from yuxi.channels.contract.dtos.transport import (
|
||||
PollResult,
|
||||
PollingConfig,
|
||||
StreamConfig,
|
||||
StreamConnection,
|
||||
TransportConfig,
|
||||
TransportErrorCategory,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.streaming import (
|
||||
ChunkMode,
|
||||
ChunkResult,
|
||||
@ -390,6 +382,14 @@ from yuxi.channels.contract.dtos.trace import (
|
||||
TraceContext,
|
||||
TraceId,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.transport import (
|
||||
PollingConfig,
|
||||
PollResult,
|
||||
StreamConfig,
|
||||
StreamConnection,
|
||||
TransportConfig,
|
||||
TransportErrorCategory,
|
||||
)
|
||||
from yuxi.channels.contract.dtos.truncation import (
|
||||
TruncationCandidate,
|
||||
TruncationResult,
|
||||
|
||||
@ -15,38 +15,46 @@ from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator
|
||||
from yuxi.channels.contract.errors import ValidationError
|
||||
|
||||
|
||||
class ChannelType(StrEnum):
|
||||
"""渠道类型。
|
||||
class ChannelType(str):
|
||||
"""渠道类型标识符。
|
||||
|
||||
标识多渠道网关支持的外部渠道种类,用于路由匹配、账户管理与插件解析。
|
||||
继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
||||
|
||||
取值:
|
||||
FEISHU: 飞书。
|
||||
DINGTALK: 钉钉。
|
||||
WECOM: 企业微信。
|
||||
WEBCHAT: 内置 Web 渠道(FR-31)。
|
||||
TELEGRAM: Telegram。
|
||||
DISCORD: Discord。
|
||||
WHATSAPP: WhatsApp。
|
||||
CUSTOM: 自定义渠道。
|
||||
由插件 manifest 声明,框架层不做白名单校验。每个插件应使用唯一的
|
||||
channel_type 值,由 ``PluginRegistry`` 保证唯一性。运行时为 ``str``
|
||||
子类,可直接作为字典 key、JSON 序列化、FastAPI 参数。
|
||||
|
||||
约束:
|
||||
框架层(contract / core / application)**禁止** 在任何决策语句中
|
||||
分支到具体枚举值(仅插件自身代码可分支),渠道特性决策 **必须**
|
||||
通过 ``ChannelManifest`` 声明字段驱动(如 ``critical`` /
|
||||
``requires_dm_pairing`` / ``requires_outbound_delivery``)。
|
||||
新增渠道优先用 ``CUSTOM`` + manifest 声明,避免修改框架契约层枚举。
|
||||
- 框架层(contract / core / application)**禁止** 在任何决策语句中
|
||||
分支到具体 channel_type 值,渠道特性决策 **必须** 通过
|
||||
``ChannelManifest`` 声明字段驱动。
|
||||
- 新增渠道仅需在插件 manifest 中声明 channel_type 字符串,无需
|
||||
修改本类。
|
||||
- channel_type 值在系统中全局唯一(由 ``PluginRegistry.register``
|
||||
校验),两个插件不得共用同一 channel_type。
|
||||
"""
|
||||
|
||||
FEISHU = "feishu"
|
||||
DINGTALK = "dingtalk"
|
||||
WECOM = "wecom"
|
||||
WEBCHAT = "webchat"
|
||||
TELEGRAM = "telegram"
|
||||
DISCORD = "discord"
|
||||
WHATSAPP = "whatsapp"
|
||||
CUSTOM = "custom"
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> Any:
|
||||
"""Pydantic 核心 schema:以 ``str`` 校验后包装为 ``ChannelType``。
|
||||
|
||||
``ChannelType`` 为 ``str`` 子类(非 ``StrEnum``),Pydantic 默认
|
||||
无法为其生成 schema,导致 ``ChannelType | None`` 作为 FastAPI
|
||||
``Query`` / ``Field`` 参数或 Pydantic 模型字段时抛
|
||||
``PydanticSchemaGenerationError``。本方法以 ``str_schema`` 校验
|
||||
输入后用 ``cls`` 包装,保留 str 子类的开放值语义(支持
|
||||
``ChannelType("wechat_ilink")`` 等动态值,无需预定义枚举成员)。
|
||||
|
||||
``pydantic_core`` 在方法内延迟导入,避免在 contract 层引入硬依赖,
|
||||
保持 DTO 模块"仅依赖标准库"的约束(无 Pydantic 环境下本类仍可正常
|
||||
使用,仅失去 Pydantic 字段类型支持)。
|
||||
"""
|
||||
from pydantic_core import core_schema
|
||||
|
||||
return core_schema.no_info_after_validator_function(
|
||||
cls,
|
||||
core_schema.str_schema(),
|
||||
)
|
||||
|
||||
|
||||
class AccountStatus(StrEnum):
|
||||
|
||||
@ -198,8 +198,7 @@ class MessageContent:
|
||||
if fmt not in valid_formats:
|
||||
raise ValidationError(
|
||||
"format",
|
||||
f"unsupported format: {fmt}, must be one of: "
|
||||
f"{', '.join(f.value for f in MessageFormat)}",
|
||||
f"unsupported format: {fmt}, must be one of: {', '.join(f.value for f in MessageFormat)}",
|
||||
)
|
||||
raw_attachments = data.get("attachments", [])
|
||||
attachments = tuple(Attachment(**a) if isinstance(a, dict) else a for a in raw_attachments)
|
||||
|
||||
@ -331,7 +331,7 @@ class OutboxQueryFilter:
|
||||
"""
|
||||
result: dict[str, Any] = {}
|
||||
if self.channel_type is not None:
|
||||
result["channel_type"] = self.channel_type.value
|
||||
result["channel_type"] = self.channel_type
|
||||
if self.channel_account_id is not None:
|
||||
result["channel_account_id"] = self.channel_account_id
|
||||
if self.status is not None:
|
||||
|
||||
@ -166,7 +166,7 @@ class PluginSummary:
|
||||
plugin_id: 插件唯一标识(manifest.manifest.id)。
|
||||
name: 插件显示名称(manifest.manifest.name)。
|
||||
version: 插件版本(manifest.manifest.version)。
|
||||
channel_type: 绑定渠道类型(manifest.manifest.channel_type.value)。
|
||||
channel_type: 绑定渠道类型(manifest.manifest.channel_type)。
|
||||
state: 当前生命周期状态(LifecycleState.value)。
|
||||
"""
|
||||
|
||||
|
||||
@ -202,9 +202,9 @@ class UnsetType:
|
||||
哨兵实例 ``UNSET`` 表示"不修改该字段,保留原值"。
|
||||
"""
|
||||
|
||||
_instance: "UnsetType | None" = None
|
||||
_instance: UnsetType | None = None
|
||||
|
||||
def __new__(cls) -> "UnsetType":
|
||||
def __new__(cls) -> UnsetType:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@ -21,11 +21,8 @@ class ClientError(Error):
|
||||
error_code = "CLIENT_ERROR"
|
||||
|
||||
|
||||
class ValidationError(ClientError, ValueError):
|
||||
"""字段校验失败错误。
|
||||
|
||||
同时继承 ValueError 以兼容 Pydantic field_validator。
|
||||
"""
|
||||
class ValidationError(ClientError):
|
||||
"""字段校验失败错误。"""
|
||||
|
||||
error_code = "VALIDATION_ERROR"
|
||||
|
||||
|
||||
@ -63,9 +63,7 @@ class TransportError(Error):
|
||||
# error_code 设为实例属性以覆盖基类类变量,按 category 动态决定。
|
||||
# status_code 由基类 property 经 CHANNEL_ERROR_STATUS_MAP 查表派生,
|
||||
# 无需也无法在此赋值(基类 status_code 为只读 property,无 setter)。
|
||||
self.error_code = _TRANSPORT_ERROR_CODE.get(
|
||||
category, _TRANSPORT_DEFAULT_ERROR_CODE
|
||||
)
|
||||
self.error_code = _TRANSPORT_ERROR_CODE.get(category, _TRANSPORT_DEFAULT_ERROR_CODE)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.dtos.channel import ChannelAccount
|
||||
from yuxi.channels.contract.dtos.media import AttachmentUploadResult
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["AttachmentUploadAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ from yuxi.channels.contract.dtos.command import ChannelCommand, CommandResult
|
||||
from yuxi.channels.contract.dtos.common import MessageContent
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["CommandAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ from yuxi.channels.contract.dtos.content_review import (
|
||||
)
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["ContentModerationAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.dtos.directory import (
|
||||
)
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["DirectoryAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.dtos.doctor import (
|
||||
)
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["DoctorAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ from yuxi.channels.contract.dtos.identity import (
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
from yuxi.channels.contract.plugin.capability import CapabilityProver
|
||||
|
||||
|
||||
__all__ = ["IdentityResolverAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ from yuxi.channels.contract.dtos.common import Attachment, MessageContent, RawEv
|
||||
from yuxi.channels.contract.dtos.inbound import SignatureVerifyResult
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["InboundAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ from yuxi.channels.contract.dtos.channel import (
|
||||
)
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["LifecycleAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.dtos.login import (
|
||||
)
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["LoginAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ from yuxi.channels.contract.dtos.common import RawEvent
|
||||
from yuxi.channels.contract.dtos.mention import MentionContext, MentionTarget
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["MentionAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ from yuxi.channels.contract.dtos.outbound import FormattedMessage
|
||||
from yuxi.channels.contract.dtos.trusted import MessageOperationContext
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["MessageOpsAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ from yuxi.channels.contract.dtos.outbound import (
|
||||
from yuxi.channels.contract.dtos.outbox import MultiPartReceipt
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["OutboundAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ from typing import Protocol, runtime_checkable
|
||||
from yuxi.channels.contract.dtos.health import AdapterProbeOutcome, ProbeResult
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["ProbeableAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ from typing import Protocol, runtime_checkable
|
||||
from yuxi.channels.contract.dtos.transport import PollingConfig, PollResult
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["PullerAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ from typing import Any, Protocol, runtime_checkable
|
||||
from yuxi.channels.contract.dtos.outbound import RichMessage
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["RichMessageAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ from yuxi.channels.contract.dtos.channel import SessionInfo
|
||||
from yuxi.channels.contract.dtos.common import RawEvent
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["SessionAdapter"]
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ from yuxi.channels.contract.dtos.common import RawEvent
|
||||
from yuxi.channels.contract.dtos.status import EventType, StatusPayload
|
||||
from yuxi.channels.contract.errors import NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["StatusAdapter"]
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user