ForcePilot/backend/package/yuxi/channels/contract/policy/config_schema.py

794 lines
29 KiB
Python
Raw Normal View History

"""配置 schema 元数据契约层定义。
定义配置字段的完整 schema 元数据``CONFIG_SCHEMA``包括键类型
是否必填默认值是否可热更新与约束作为配置 schema 的唯一真相源
§13.1 配置分层 / §6.1 契约层职责适配器 ``RedisConfigAdapter``
与应用层 ``ConfigManager``共享引用本模块消除 schema 元数据
下沉至适配器层的 DRY 违反
``HOT_RELOADABLE_KEYS`` / ``NON_HOT_RELOADABLE_KEYS`` ``CONFIG_SCHEMA``
派生 ``ConfigManager`` 做热更新策略校验ADP-030
"""
from __future__ import annotations
from yuxi.channels.contract.dtos.config import ConfigField, ConfigScope
# 配置 schema全部配置字段的完整元数据FR-37 配置热更新)。
# 51 个可热更新字段 + 14 个不可热更新字段,共 65 个。
# 字段顺序先可热更新hot_reloadable=True后不可热更新hot_reloadable=False
CONFIG_SCHEMA: tuple[ConfigField, ...] = (
# === 可热更新字段hot_reloadable=True===
ConfigField(
key="dm_policy",
type="json",
required=False,
default="allow",
hot_reloadable=True,
title="私信策略",
description="控制私信/群聊的自动回复与路由策略,如是否允许陌生人私信、是否自动创建会话等。",
category="消息策略",
),
ConfigField(
key="allow_from",
type="json",
required=False,
default=None,
hot_reloadable=True,
title="来源白名单",
description="允许向当前账户发起对话的对端白名单,未配置时不限制。",
category="访问控制",
),
ConfigField(
key="rate_limit",
type="json",
required=False,
default=None,
hot_reloadable=True,
title="消息限流",
description="控制账户级别的消息发送频率限制,防止滥用或触发渠道风控。",
category="限流与预算",
),
ConfigField(
key="bot_loop_budget",
type="json",
required=False,
default={"max_replies_per_hour": 20, "cooldown_seconds": 60},
hot_reloadable=True,
title="Bot 循环预算",
description=(
"Bot 循环预算配置,包含 max_replies_per_hour"
"每小时最大回复数≤0 表示不限制)和 cooldown_seconds冷却秒数"
),
category="限流与预算",
),
# FR-08~14 功能开关Task 30
ConfigField(
key="rich_message_enabled",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="富消息开关",
description="是否启用富媒体消息(卡片、按钮等)发送能力。",
category="功能开关",
),
ConfigField(
key="status_writeback_enabled",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="状态回写开关",
description="是否将渠道用户状态(在线/离开等)回写到知识库。",
category="功能开关",
),
ConfigField(
key="agent_prompt_enabled",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="Agent 提示开关",
description="是否在消息中附加 Agent 提示上下文。",
category="功能开关",
),
ConfigField(
key="channel_tools_enabled",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="渠道工具开关",
description="是否允许 Agent 调用渠道相关工具。",
category="功能开关",
),
# AC-22: 消息操作开关默认"开"FR-12
ConfigField(
key="message_ops_enabled",
type="bool",
required=False,
default=True,
hot_reloadable=True,
title="消息操作开关",
description="总开关:是否启用消息操作(反应、置顶、卡片更新等)。",
category="功能开关",
),
ConfigField(
key="streaming_enabled",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="流式响应开关",
description="是否启用流式(打字机效果)响应输出。",
category="功能开关",
),
ConfigField(
key="enable_typing",
type="bool",
required=False,
default=True,
hot_reloadable=True,
title="输入中提示开关",
description="是否向用户展示'正在输入'状态提示。",
category="功能开关",
),
ConfigField(
key="directory_enabled",
type="bool",
required=False,
default=True,
hot_reloadable=True,
title="目录服务开关",
description="是否启用渠道目录(用户/群组列表)查询能力。",
category="功能开关",
),
ConfigField(
key="command_enabled",
type="bool",
required=False,
default=True,
hot_reloadable=True,
title="命令处理开关",
description="是否启用以 command_prefix 开头的命令解析与处理。",
category="功能开关",
),
# FR-12 消息操作细粒度开关PRD §FR-12 配置项)。
# AC-22 默认行为:表情反应"开"、置顶"关"、卡片更新"关"。
ConfigField(
key="enable_reaction",
type="bool",
required=False,
default=True,
hot_reloadable=True,
title="表情反应开关",
description="是否允许对消息添加表情反应。",
category="功能开关",
),
ConfigField(
key="enable_pin",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="消息置顶开关",
description="是否允许置顶/取消置顶消息。",
category="功能开关",
),
ConfigField(
key="enable_card_update",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="卡片更新开关",
description="是否允许更新已发送的卡片消息。",
category="功能开关",
),
# FR-15 管理员命令权限白名单账户级JSON 数组,存放允许执行 ADMIN 权限命令的 peer_id
ConfigField(
key="admin_users",
type="json",
required=False,
default=None,
hot_reloadable=True,
scope=ConfigScope.ACCOUNT,
title="管理员白名单",
description="允许执行 ADMIN 权限命令的 peer_id 列表,账户级配置。",
category="身份与权限",
),
# FR-29 入站提及策略账户级JSON含 allowed_implicit_kinds 列表,
# 控制哪些隐式提及类型被视为"已提及",未配置时默认空策略)
ConfigField(
key="mention_policy",
type="json",
required=False,
default=None,
hot_reloadable=True,
scope=ConfigScope.ACCOUNT,
title="提及策略",
description="控制哪些隐式提及类型被视为'已提及',影响命令触发与会话路由。",
category="消息策略",
),
# FR-07 会话合并策略开关(默认禁用)
ConfigField(
key="merge_strategy_enabled",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="会话合并开关",
description="是否启用按策略自动合并会话。",
category="会话策略",
),
# FR-06 跨渠道身份策略(默认隔离,可选 association 开启跨渠道关联)
ConfigField(
key="cross_channel_identity_strategy",
type="str",
required=False,
default="isolation",
hot_reloadable=True,
title="跨渠道身份策略",
description="跨渠道身份关联策略isolation隔离或 association关联",
category="身份与权限",
),
# 可配置值Task 30
ConfigField(
key="command_prefix",
type="str",
required=False,
default="/",
hot_reloadable=True,
title="命令前缀",
description="命令触发前缀,默认为 '/'",
category="命令与交互",
),
ConfigField(
key="fence_ttl_seconds",
type="int",
required=False,
default=120,
hot_reloadable=True,
title="栅栏 TTL",
description="对话栅栏fence的默认过期时间",
category="命令与交互",
scope=ConfigScope.GLOBAL,
),
ConfigField(
key="fence_fail_closed",
type="bool",
required=False,
default=False,
hot_reloadable=True,
title="栅栏 fail-closed 模式",
description="栅栏故障 fail-closed 模式true 时 Redis 故障拒绝投递。",
category="命令与交互",
scope=ConfigScope.GLOBAL,
),
ConfigField(
key="streaming_min_chunk_interval_ms",
type="int",
required=False,
default=200,
hot_reloadable=True,
title="流式最小间隔",
description="流式响应相邻分块之间的最小间隔(毫秒)。",
category="流式与传输体验",
),
ConfigField(
key="streaming_ttl_seconds",
type="int",
required=False,
default=60,
hot_reloadable=True,
title="流式 TTL",
description="流式响应状态的最大保留时间(秒)。",
category="流式与传输体验",
),
ConfigField(
key="typing_ttl_ms",
type="int",
required=False,
default=10000,
hot_reloadable=True,
title="输入提示 TTL",
description="'正在输入'提示状态的最大持续时间(毫秒)。",
category="流式与传输体验",
),
ConfigField(
key="durability_policy_default",
type="str",
required=False,
default="required",
hot_reloadable=True,
title="默认持久化策略",
description="消息默认持久化策略required / best_effort / none。",
category="可靠性",
),
# FR-24 分阶段 ACK 策略PRD §FR-24
# 四阶段策略after_record / after_agent_dispatch / after_persist / manual
# 默认 after_agent_dispatch平衡可靠性与响应速度
ConfigField(
key="ack_policy",
type="str",
required=False,
default="after_agent_dispatch",
hot_reloadable=True,
title="ACK 策略",
description="分阶段 ACK 策略after_record / after_agent_dispatch / after_persist / manual。",
category="可靠性",
),
# 手动策略下插件忘记 ACK 时核心兜底超时(默认 60s自动 ACK。
ConfigField(
key="ack_fallback_timeout_seconds",
type="int",
required=False,
default=60,
hot_reloadable=True,
title="ACK 兜底超时",
description="手动 ACK 策略下,插件忘记 ACK 时核心自动 ACK 的超时时间(秒)。",
category="可靠性",
),
# 传输引擎配置
ConfigField(
key="transport.stall_timeout_ms",
type="int",
required=False,
default=120000,
hot_reloadable=True,
title="传输停滞超时",
description="传输流判定为停滞的超时时间(毫秒)。",
category="传输引擎",
),
ConfigField(
key="transport.backoff_schedule",
type="str",
required=False,
default="1,2,5,10,30",
hot_reloadable=True,
title="传输退避序列",
description="传输重试的退避时间序列,逗号分隔的秒数列表。",
category="传输引擎",
),
ConfigField(
key="transport.backoff_jitter",
type="float",
required=False,
default=0.2,
hot_reloadable=True,
title="传输退避抖动",
description="传输重试退避的随机抖动系数。",
category="传输引擎",
),
# StreamWorker 断线重连指数退避参数Task 10.3),与 TransportConfig
# DTO 字段一一对应,由 base_worker._loadConfig 通过 ConfigPort 读取。
ConfigField(
key="transport.stream_reconnect_backoff_ms",
type="int",
required=False,
default=1000,
hot_reloadable=True,
title="流重连初始退避",
description="StreamWorker 断线重连的指数退避基数(毫秒),每次重连翻倍直至达到最大退避上限。",
category="传输引擎",
),
ConfigField(
key="transport.stream_reconnect_max_backoff_ms",
type="int",
required=False,
default=30000,
hot_reloadable=True,
title="流重连最大退避",
description="StreamWorker 断线重连指数退避的上限(毫秒),避免退避时间无限增长。",
category="传输引擎",
),
# FR-22 Outbox 重试策略OBX-POLICY-UPDATEJSON 格式:
# {"max_retry": int, "ttl_seconds": int, "retry_backoff_schedule": [int, ...]}
# 由 OutboxHandler.updateRetryPolicy 双写到 ConfigPortRedis
# 支持跨请求 / 跨进程最终一致性。
ConfigField(
key="outbox_retry_policy",
type="json",
required=False,
default=None,
hot_reloadable=True,
title="Outbox 重试策略",
description="出站消息重试策略,包含 max_retry / ttl_seconds / retry_backoff_schedule。",
category="可靠性",
),
# FR-22 Outbox Scanner-Worker 协调Task 10Scanner 判定
# SENT_UNCONFIRMED 无回执超时与 Worker 重试锁 TTL原为硬编码
# 300s/60s现统一收敛至 CONFIG_SCHEMA支持热更新。
ConfigField(
key="outbox_sent_unconfirmed_timeout_seconds",
type="int",
required=False,
default=300,
hot_reloadable=True,
title="Outbox SENT_UNCONFIRMED 超时(秒)",
description="Scanner 判定 SENT_UNCONFIRMED 消息无回执的超时秒数,超过此时间视为可能已发送但无回执",
category="可靠性",
),
ConfigField(
key="outbox_retry_lock_ttl_seconds",
type="int",
required=False,
default=60,
hot_reloadable=True,
title="Outbox 重试锁 TTL",
description="Worker 单条消息重试的分布式锁 TTL覆盖一次网络调用+DB持久化+事件发布的完整周期",
category="可靠性",
),
# === 渠道用户与 Agent 交互链路优化Task 6/7/8===
# 身份解析置信度阈值账户级scope=ACCOUNT低于此阈值的解析结果视为
# 未命中,进入 channel_guest 身份生成路径。缺口补齐:原 _getThreshold
# 使用硬编码默认值,现统一收敛至 CONFIG_SCHEMA。
ConfigField(
key="identity_confidence_threshold",
type="float",
required=False,
default=0.5,
hot_reloadable=True,
scope=ConfigScope.ACCOUNT,
title="身份置信度阈值",
description="身份解析结果低于此阈值时视为未命中,进入 channel_guest 生成路径。",
category="身份与权限",
),
# 新建 Agent 的默认渠道访问级别全局级scope=GLOBAL取值如
# "none" / "read" / "write" 等,由 Agent 创建流程读取。
ConfigField(
key="channel_access_default_level",
type="str",
required=False,
default="none",
hot_reloadable=True,
scope=ConfigScope.GLOBAL,
title="默认渠道访问级别",
description="新建 Agent 默认的渠道访问级别,如 none / read / write。",
category="身份与权限",
),
# 渠道访客身份置信度全局级scope=GLOBALchannel_guest 身份生成时
# 使用此置信度,低于 identity_confidence_threshold 以避免被误判为
# 已解析身份。
ConfigField(
key="channel_guest_confidence",
type="float",
required=False,
default=0.3,
hot_reloadable=True,
scope=ConfigScope.GLOBAL,
title="访客身份置信度",
description="channel_guest 身份生成时使用的置信度,应低于身份置信度阈值。",
category="身份与权限",
),
# 是否启用管理员手动绑定功能全局级scope=GLOBAL控制渠道用户与
# 统一身份的手动绑定入口是否可用。
ConfigField(
key="enable_channel_user_binding",
type="bool",
required=False,
default=True,
hot_reloadable=True,
scope=ConfigScope.GLOBAL,
title="手动绑定开关",
description="是否启用管理员手动绑定渠道用户与统一身份的功能入口。",
category="身份与权限",
),
# === 渠道账号运营流程优化Task 14契约层 CONFIG_SCHEMA 健康检查配置键)===
# 以下 2 个键均为渠道级scope=CHANNEL归属 ConfigPortRedis 热重载,
# CHANNEL 业务策略体系),不得声明在 app_config。
# 凭证失效计数阈值。健康检查探活返回 401/403 时计入凭证失效计数,
# 达阈值后调聚合根 markFailed。窗口外credential_failure_window_seconds
# 重置计数。
ConfigField(
key="credential_failure_threshold",
type="int",
required=False,
default=3,
hot_reloadable=True,
scope=ConfigScope.CHANNEL,
title="凭证失效阈值",
description="健康检查中凭证失效计数的阈值。",
category="健康检查",
),
# 凭证失效计数窗口(秒)。窗口外的失效计数被重置,避免历史故障永久影响账号状态。
ConfigField(
key="credential_failure_window_seconds",
type="int",
required=False,
default=300,
hot_reloadable=True,
scope=ConfigScope.CHANNEL,
title="凭证失效窗口",
description="凭证失效计数的滑动窗口时长(秒)。",
category="健康检查",
),
# 审计日志保留策略全局级scope=GLOBAL。JSON 格式:
# {"retention_days": int, "cleanup_interval_hours": int}
# 控制审计日志的保留天数与清理周期,由审计清理任务读取。
ConfigField(
key="audit_retention_policy",
type="json",
required=False,
default={"retention_days": 90, "cleanup_interval_hours": 24},
hot_reloadable=True,
scope=ConfigScope.GLOBAL,
title="审计保留策略",
description="审计日志保留天数与清理周期配置JSON 格式。",
category="审计与登录",
),
# 扫码登录重定向 URI全局级scope=GLOBAL。扫码登录成功后的前端跳转地址
# 为空时使用默认跳转行为。
ConfigField(
key="qr_login_redirect_uri",
type="str",
required=False,
default="",
hot_reloadable=True,
scope=ConfigScope.GLOBAL,
title="扫码登录跳转地址",
description="扫码登录成功后的前端跳转地址。",
category="审计与登录",
),
# 渠道熔断器配置GLOBAL 作用域),由 ChannelCircuitBreaker 读取。
# 包含连续失败阈值与恢复超时时间;未配置时使用组件内默认值。
ConfigField(
key="circuit_breaker",
type="json",
required=False,
default={"threshold": 5, "recovery_timeout": 60.0},
hot_reloadable=True,
scope=ConfigScope.GLOBAL,
title="渠道熔断器配置",
description="渠道 API 熔断器参数,包含连续失败阈值与恢复超时时间。",
category="可靠性",
),
# === 不可热更新字段hot_reloadable=False===
ConfigField(
key="webhook_port",
type="int",
required=False,
default=None,
hot_reloadable=False,
title="Webhook 端口",
description="渠道 Webhook 服务监听端口。",
category="基础设施",
),
ConfigField(
key="webhook_secret",
type="str",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
title="Webhook 密钥",
description="渠道 Webhook 签名验证密钥,敏感字段。",
category="基础设施",
),
ConfigField(
key="tls_cert",
type="str",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
title="TLS 证书",
description="TLS 证书内容,敏感字段。",
category="基础设施",
),
ConfigField(
key="tls_key",
type="str",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
title="TLS 私钥",
description="TLS 私钥内容,敏感字段。",
category="基础设施",
),
ConfigField(
key="plugin_entry",
type="str",
required=False,
default=None,
hot_reloadable=False,
title="插件入口",
description="插件入口模块路径。",
category="基础设施",
),
ConfigField(
key="database_url",
type="str",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
title="数据库地址",
description="业务数据库连接地址,敏感字段。",
category="基础设施",
),
ConfigField(
key="redis_url",
type="str",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
title="Redis 地址",
description="Redis 连接地址,敏感字段。",
category="基础设施",
),
ConfigField(
key="transport.max_restart_attempts",
type="int",
required=False,
default=None,
hot_reloadable=False,
title="最大重启尝试",
description="传输进程异常后的最大重启次数。",
category="传输引擎",
),
ConfigField(
key="transport.graceful_shutdown_timeout_s",
type="float",
required=False,
default=10.0,
hot_reloadable=False,
title="优雅关闭超时",
description="传输进程优雅关闭的最大等待时间(秒)。",
category="传输引擎",
),
# === 渠道账号运营流程优化Task 3契约层 CONFIG_SCHEMA 凭证配置键)===
# 以下 3 个键均为账户级scope=ACCOUNT归属 ConfigPortRedis 热重载,
# CHANNEL 业务策略/toggles/TTLs 体系),不得声明在 app_config。
# 凭证包,敏感字段加密存储。由 CredentialService 唯一写入入口维护,
# 包含 bot_token/app_secret/user_access_token 等渠道认证凭证。
ConfigField(
key="credentials",
type="dict",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
scope=ConfigScope.ACCOUNT,
title="渠道凭证包",
description="渠道认证凭证集合,敏感字段。",
category="凭证与登录态",
),
# 登录态unlogged/logging_in/logged_in/logout_failed。由 QrLoginService
# 通过 ConfigPort 维护,区分扫码会话状态与最终凭证落库状态。
ConfigField(
key="login_status",
type="str",
required=False,
default="unlogged",
hot_reloadable=False,
scope=ConfigScope.ACCOUNT,
title="登录状态",
description="当前账户扫码登录状态。",
category="凭证与登录态",
),
# 凭证版本号,每次 rotateCredentials 自增。用于乐观锁控制与缓存失效判定。
ConfigField(
key="credential_version",
type="int",
required=False,
default=0,
hot_reloadable=False,
scope=ConfigScope.ACCOUNT,
title="凭证版本",
description="凭证轮换版本号,用于乐观锁与缓存失效。",
category="凭证与登录态",
),
# FR17-P0-3 已应用迁移列表ACCOUNT 作用域target=channel_type 或
# {channel_type}:{account_id})。由 PluginLifecycleManager 在插件 onStart
# 成功后追加 manifest.version 标记配置已加载DoctorService 只读不写。
# hot_reloadable=False非业务策略启动期写入无需热更新。
# default=[] 让首次加载时 ConfigPort.get 返回空列表而非 NotFoundError
# 避免 PluginLifecycleManager 与 DoctorService 的 catch 块接不到异常。
ConfigField(
key="applied_migrations",
type="json",
required=False,
default=[],
hot_reloadable=False,
scope=ConfigScope.ACCOUNT,
title="已应用迁移",
description="账户已应用的插件迁移版本列表。",
category="凭证与登录态",
),
# 渠道命令声明CHANNEL 作用域),由各渠道 CommandAdapter 读取。
# 存储命令列表 JSON未配置时适配器回退为内置默认命令。
ConfigField(
key="commands",
type="json",
required=False,
default=None,
hot_reloadable=True,
scope=ConfigScope.CHANNEL,
title="渠道命令列表",
description="渠道自定义命令声明,未配置时使用内置默认命令。",
category="功能开关",
),
# 安装向导状态CHANNEL 作用域),由 WizardService 读写。
# 存储已应用步骤的配置补丁聚合,支持跨会话恢复向导进度。
ConfigField(
key="wizard_state",
type="json",
required=False,
default={},
hot_reloadable=True,
scope=ConfigScope.CHANNEL,
title="向导状态",
description="安装向导已应用步骤的配置补丁聚合,用于跨会话恢复向导进度。",
category="运营流程",
),
# 安装向导最终配置CHANNEL 作用域),由 WizardService.finalize 写入。
# 存储向导聚合后的完整渠道配置,供账户创建后运行时读取。
ConfigField(
key="channel_config",
type="json",
required=False,
default=None,
hot_reloadable=True,
scope=ConfigScope.CHANNEL,
title="渠道向导配置",
description="安装向导 finalize 后写入的聚合渠道配置,供运行时读取。",
category="运营流程",
),
# 用户级访问令牌ACCOUNT 作用域),由 LoginAdapter 在登出时读取。
# 企业微信 OAuth 无独立 user_access_token此键供 logout 检测登录态存在性。
ConfigField(
key="user_access_token",
type="str",
required=False,
default=None,
hot_reloadable=False,
sensitive=True,
scope=ConfigScope.ACCOUNT,
title="用户访问令牌",
description="用户级 OAuth 访问令牌,用于登录态检测与登出清理。",
category="凭证与登录态",
),
# 显式身份映射ACCOUNT 作用域),由 ExplicitMappingResolver 读取。
# 管理员配置 peer_id → 统一身份的映射置信度最高1.0)。
# 格式:{peer_id: {"identity_id": "...", "identity_type": "...", "user_id": "..."}}
ConfigField(
key="identity_mapping",
type="json",
required=False,
default=None,
hot_reloadable=True,
scope=ConfigScope.ACCOUNT,
title="显式身份映射",
description="管理员配置的 peer_id 到统一身份的映射置信度最高用于显式身份解析FR-05",
category="身份与路由",
),
# 路由 Agent 覆盖绑定ACCOUNT 作用域),由 RouteResolver 配置回退链读取。
# 管理员配置 session_key 派生键 → agent slug 的映射,按会话粒度覆盖默认 agent 路由。
# 键可为完整 session_keydirect、归一化键normalized_direct、父级键parent
# 归一化父级键normalized_parent或通配符 "*"wildcard
# 格式:{"wechat_woc:acc1:peer_id": "agent-slug", "*": "default-bot"}
ConfigField(
key="route_agent_bindings",
type="json",
required=False,
default=None,
hot_reloadable=True,
scope=ConfigScope.ACCOUNT,
title="路由 Agent 覆盖绑定",
description="按会话粒度覆盖默认 agent 路由的绑定映射由路由配置回退链读取FR-30",
category="身份与路由",
),
)
# 可热更新的配置项键集合FR-37从 CONFIG_SCHEMA 派生。
HOT_RELOADABLE_KEYS = frozenset(f.key for f in CONFIG_SCHEMA if f.hot_reloadable)
# 不可热更新的配置项键集合FR-37从 CONFIG_SCHEMA 派生。
NON_HOT_RELOADABLE_KEYS = frozenset(f.key for f in CONFIG_SCHEMA if not f.hot_reloadable)
__all__ = ["CONFIG_SCHEMA", "HOT_RELOADABLE_KEYS", "NON_HOT_RELOADABLE_KEYS"]