1 line
1.3 MiB
Plaintext
1 line
1.3 MiB
Plaintext
{"repo": "asakitan/cattyqqbot", "files": {"src/catty_qq_ai/config.py": {"25271da2cc17f0cc1a8e977cefef655cd510a9e1": {"t": 1785746873.2081695, "n": 48007, "h": "3ad30e9d3fd8db7d", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n # ── 主人 2026-07-06 多 provider 缓存适配 (openai-claude-95 计划) ─────────\n # OpenAI 隐式缓存路由亲和: 仅对 detect_provider=='openai' 的端点注入\n # payload.prompt_cache_key = \"catty:{scope}\"。DeepSeek 绝不注入 (Round 10 红线:\n # user/prompt_cache_key 会把 DeepSeek 公共前缀 cache 分裂成独立 namespace 各自冷启)。\n catty_openai_prompt_cache_key_enabled: bool = False\n # native 路由 per-line 覆盖: key ∈ {main,spark,filter,summary_fallback,vision,router},\n # value ∈ {auto,native,compat}; 缺省 auto = detect_provider(线路 base_url, model) 判别。\n # 只在 catty_anthropic_native_enabled=True (总闸) 时整个 native 路由才激活。\n catty_native_route_overrides: dict[str, str] = Field(default_factory=dict)\n # per-line cache TTL (\"5min\"|\"1h\"), 缺省落全局 catty_cache_ttl。\n # 目标态 {\"main\":\"1h\",\"spark\":\"1h\"} — QQ 轮间隔常 >5min, 5min TTL 过期重写吃掉\n # 命中 (读会续期, 只有间隔>TTL 的轮全量重写)。待 A/B 实测后写进远端 config。\n catty_cache_ttl_overrides: dict[str, str] = Field(default_factory=dict)\n # native 末尾 assistant prefill 模式: \"hint\"=现状字节等价 (丢 prefill 转强 IC hint\n # 进最近 user); \"native\"=真 trailing assistant 续写; \"auto\"=按模型名分派。\n catty_native_prefill_mode: str = \"hint\"\n # native 额外 anthropic-beta (逃生阀: 某些中转要显式 extended-cache-ttl-2025-04-11\n # 才认 1h TTL 等), 附加在默认 betas 之后。\n catty_native_extra_betas: list[str] = Field(default_factory=list)\n # A/B 测试 provider 注册表: {name: {base_url, api_key, model, native?(\"1\")}}。\n # 仅 /dev/sim_chat 的 provider_override 按名引用 — 凭据留在 config, 不过 HTTP。\n catty_test_providers: dict[str, dict[str, str]] = Field(default_factory=dict)\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # 时间桶上下文 sidecar: 不改旧 sessions 格式, 把跨时间段的旧 raw 对话降温成稳定摘要,\n # 当前桶只注入短参数, 让 DeepSeek prefix/KV 长时间稳定命中。\n catty_time_bucket_context_enabled: bool = True\n catty_time_bucket_context_dir: str = \"session_buckets\"\n catty_time_bucket_group_minutes: int = 15\n catty_time_bucket_private_minutes: int = 30\n catty_time_bucket_max_finalized: int = 8\n catty_time_bucket_max_turns_per_bucket: int = 24\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── Token 计费 (主人 2026-07-06, 取代上面已随 CPU 引擎关停的 credit 链路) ──\n # 私聊: 每次回复按本轮全部 AI 调用的 prompt+completion token 扣积分,\n # 每 tokens_per_point 个 token 扣 1 分 (向上取整); 余额 <=0 拦截要签到.\n # 群聊: 不扣积分, 每人每小时 quota 个 token 额度 (整点桶), 超了拦截,\n # 每小时只提示一次. 主人全豁免. 两者与 catty_credit_enabled 互不相干.\n # 拦截提醒 AI 现写 (token_billing.ai_gate_reply), 失败兜底固定文案池.\n catty_token_billing_enabled: bool = True\n catty_private_tokens_per_point: int = 1000\n catty_group_hourly_token_quota: int = 300_000 # 0 = 群聊不限\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 1440\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n # ── 多人格 (主人 2026-07-06): 群号→人格名映射 + 全局默认人格 ──\n # 解析优先级: persona_override_store(/人格 命令) > group_personas[gid] > default_persona。\n # 人格名见 personas.PERSONAS (catty / fadianji), 支持中文别名 (机机/笨猫)。\n catty_group_personas: dict[str, str] = Field(default_factory=dict)\n catty_default_persona: str = \"catty\"\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\", \"catty_native_extra_betas\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n \"catty_native_route_overrides\",\n \"catty_cache_ttl_overrides\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_test_providers\", mode=\"before\")\n @classmethod\n def parse_test_providers(cls, value: Any) -> Any:\n # {name: {base_url, api_key, model, native?}} — env 里是 JSON 字符串 (hot reload\n # 走 Config.model_validate(raw env str), 必须 before-validator 解析)\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = dict(value)\n out: dict[str, dict[str, str]] = {}\n for name, entry in data.items():\n if isinstance(entry, dict):\n out[str(name)] = {str(k): str(v) for k, v in entry.items()}\n return out\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", \"catty_group_personas\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "385c49c3cf3b85a0c2c263c211a58163ce41ce7a": {"t": 1785768340.9717455, "n": 45691, "h": "c1d7524da3f2ef9e", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # 时间桶上下文 sidecar: 不改旧 sessions 格式, 把跨时间段的旧 raw 对话降温成稳定摘要,\n # 当前桶只注入短参数, 让 DeepSeek prefix/KV 长时间稳定命中。\n catty_time_bucket_context_enabled: bool = True\n catty_time_bucket_context_dir: str = \"session_buckets\"\n catty_time_bucket_group_minutes: int = 15\n catty_time_bucket_private_minutes: int = 30\n catty_time_bucket_max_finalized: int = 8\n catty_time_bucket_max_turns_per_bucket: int = 24\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── Token 计费 (主人 2026-07-06, 取代上面已随 CPU 引擎关停的 credit 链路) ──\n # 私聊: 每次回复按本轮全部 AI 调用的 prompt+completion token 扣积分,\n # 每 tokens_per_point 个 token 扣 1 分 (向上取整); 余额 <=0 拦截要签到.\n # 群聊: 不扣积分, 每人每小时 quota 个 token 额度 (整点桶), 超了拦截,\n # 每小时只提示一次. 主人全豁免. 两者与 catty_credit_enabled 互不相干.\n # 拦截提醒 AI 现写 (token_billing.ai_gate_reply), 失败兜底固定文案池.\n catty_token_billing_enabled: bool = True\n catty_private_tokens_per_point: int = 1000\n catty_group_hourly_token_quota: int = 300_000 # 0 = 群聊不限\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 1440\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n # ── 多人格 (主人 2026-07-06): 群号→人格名映射 + 全局默认人格 ──\n # 解析优先级: persona_override_store(/人格 命令) > group_personas[gid] > default_persona。\n # 人格名见 personas.PERSONAS (catty / fadianji), 支持中文别名 (机机/笨猫)。\n catty_group_personas: dict[str, str] = Field(default_factory=dict)\n catty_default_persona: str = \"catty\"\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", \"catty_group_personas\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "93208ae8cf7661dceed0cb667ada5696a89e9a41": {"t": 1785768342.512315, "n": 45185, "h": "8242bd234db809d1", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # 时间桶上下文 sidecar: 不改旧 sessions 格式, 把跨时间段的旧 raw 对话降温成稳定摘要,\n # 当前桶只注入短参数, 让 DeepSeek prefix/KV 长时间稳定命中。\n catty_time_bucket_context_enabled: bool = True\n catty_time_bucket_context_dir: str = \"session_buckets\"\n catty_time_bucket_group_minutes: int = 15\n catty_time_bucket_private_minutes: int = 30\n catty_time_bucket_max_finalized: int = 8\n catty_time_bucket_max_turns_per_bucket: int = 24\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 1440\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n # ── 多人格 (主人 2026-07-06): 群号→人格名映射 + 全局默认人格 ──\n # 解析优先级: persona_override_store(/人格 命令) > group_personas[gid] > default_persona。\n # 人格名见 personas.PERSONAS (catty / fadianji), 支持中文别名 (机机/笨猫)。\n catty_group_personas: dict[str, str] = Field(default_factory=dict)\n catty_default_persona: str = \"catty\"\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", \"catty_group_personas\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "a233a91b88909bafac9e9c09d2cd3b8c00006568": {"t": 1785768344.1851234, "n": 45161, "h": "848838f209424fa8", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # 时间桶上下文 sidecar: 不改旧 sessions 格式, 把跨时间段的旧 raw 对话降温成稳定摘要,\n # 当前桶只注入短参数, 让 DeepSeek prefix/KV 长时间稳定命中。\n catty_time_bucket_context_enabled: bool = True\n catty_time_bucket_context_dir: str = \"session_buckets\"\n catty_time_bucket_group_minutes: int = 15\n catty_time_bucket_private_minutes: int = 30\n catty_time_bucket_max_finalized: int = 8\n catty_time_bucket_max_turns_per_bucket: int = 24\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 1440\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n # ── 多人格 (主人 2026-07-06): 群号→人格名映射 + 全局默认人格 ──\n # 解析优先级: persona_override_store(/人格 命令) > group_personas[gid] > default_persona。\n # 人格名见 personas.PERSONAS (catty / fadianji), 支持中文别名 (机机/笨猫)。\n catty_group_personas: dict[str, str] = Field(default_factory=dict)\n catty_default_persona: str = \"catty\"\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "fbdbdcbfb070f9bbcaf92c78e747e045c7ddd319": {"t": 1785768345.8991268, "n": 44847, "h": "1ed7ff12c7ebb471", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # 时间桶上下文 sidecar: 不改旧 sessions 格式, 把跨时间段的旧 raw 对话降温成稳定摘要,\n # 当前桶只注入短参数, 让 DeepSeek prefix/KV 长时间稳定命中。\n catty_time_bucket_context_enabled: bool = True\n catty_time_bucket_context_dir: str = \"session_buckets\"\n catty_time_bucket_group_minutes: int = 15\n catty_time_bucket_private_minutes: int = 30\n catty_time_bucket_max_finalized: int = 8\n catty_time_bucket_max_turns_per_bucket: int = 24\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 1440\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "7fc1b50ad9cba34c0993b70af05db4d6427d9df1": {"t": 1785768347.4860547, "n": 44845, "h": "f55fb64f9f25fcc8", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # 时间桶上下文 sidecar: 不改旧 sessions 格式, 把跨时间段的旧 raw 对话降温成稳定摘要,\n # 当前桶只注入短参数, 让 DeepSeek prefix/KV 长时间稳定命中。\n catty_time_bucket_context_enabled: bool = True\n catty_time_bucket_context_dir: str = \"session_buckets\"\n catty_time_bucket_group_minutes: int = 15\n catty_time_bucket_private_minutes: int = 30\n catty_time_bucket_max_finalized: int = 8\n catty_time_bucket_max_turns_per_bucket: int = 24\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "539923513f0bc8ae6748c32f4ee254c7a73797df": {"t": 1785768349.1347058, "n": 44434, "h": "71fac5241f73ef25", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n # 主人 2026-05-29 v5: 改 1 (串行排队), 上一条完成下一条再发. OLLAMA_NUM_PARALLEL=1\n # 多并发会让每个请求都被前面卡住 → 排队场景下 timeout. 串行保证每个 warm 准.\n catty_cpu_engine_l4_catnify_concurrency: int = 1\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "72ed075d9f4237b323273eceb6878df24823e5a2": {"t": 1785768350.7438517, "n": 44310, "h": "92e72ace780b7448", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 v4: 3B 演技不够还原 (echo prompt 字面字符) → 切 7B.\n # qwen2.5:7b 4 核 affinity warm 5.6s / decode 5.4 tok/s / 角色扮演显著强于 3B.\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:7b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n # 主人 2026-05-29 v4: 7B 5.4 tok/s × 200 = 37s decode 超时. 改 80 让笨猫回复短.\n catty_cpu_engine_l4_catnify_max_tokens: int = 80\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n catty_cpu_engine_l4_catnify_concurrency: int = 2\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "6efc63b6dbaeadb806b74c9fe61c044b7c2926a3": {"t": 1785768352.4583054, "n": 44253, "h": "594aeedb08c0f77c", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n # 主人 2026-05-29 S5.6e: 实测 qwen3 thinking 关不掉超时, qwen2.5:1.5b 扮演弱.\n # 最终选 qwen2.5:3b: warm 2.85s / decode 11.6 tok/s / 笨猫扮演完美 (4 核 affinity 下).\n catty_cpu_engine_l4_catnify_model: str = \"qwen2.5:3b\"\n # S5.6f (主人 2026-05-29) BUG FIX: catnify 必须走本地 Ollama, 不能复用 ai_fallback\n # (主人 ai_fallback 配的是 deepseek.com 兜底, catnify 调它会跑去 DeepSeek 烧 token).\n catty_cpu_engine_l4_catnify_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_cpu_engine_l4_catnify_api_key: str = \"ollama\"\n # S5.6f (主人 2026-05-29): 实测 qwen2.5:3b cold load 4.9s (磁盘→内存),\n # warm 2.85s; timeout 8s 留 cold 余地, 主人 ≤10s 上限内.\n catty_cpu_engine_l4_catnify_timeout_ms: int = 8000\n # 主人 2026-05-29 v2: token 输入只能 < 1000 (6C CPU prefill 慢, 保守 800 留余量)\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 800\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n catty_cpu_engine_l4_catnify_max_tokens: int = 200\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n catty_cpu_engine_l4_catnify_concurrency: int = 2\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # openai_client 的回复入口 (chat_completion / _with_tools / _instant / _codex_instant)\n # 拿到 DeepSeek 生成的【面向用户回复】后异步追加, 累积 N 条触发 L3 index reload.\n # 主人 2026-05-29 决策: 私聊 (含 NSFW) 也采, 全采 — 只在写入前脱敏 (@/QQ号/称呼).\n # filter/分类/判断 (输出 bool/JSON) 不采 (埋点不在那些入口, 见 corpus_distill 模块头).\n catty_cpu_engine_l3_distill_enabled: bool = True\n # 主人 2026-05-29: 默认 False = 私聊也采 (全采). 之前误为 True 导致私聊全被跳过.\n catty_cpu_engine_l3_distill_skip_private: bool = False\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n # 主人 2026-05-29: 50 太高 (单链路时 pending 卡 11/50 永不 rebuild). 现在全链路采集 +\n # 调到 12, 让新蒸馏的语料更快进 L3 index. 热重载可随时改 config.json 即生效.\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 12\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "a721adb6859722a015fc020eb206bd516c28ae89": {"t": 1785768353.9978724, "n": 43232, "h": "1b256819239844f7", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # ── S5 (2026-05-29 主人方案): 本地 CPU LLM 全量笨猫体质改写 ───────────\n # mode:\n # off — L4 不动 (S1-S4 行为, 强互动直走 L5)\n # stylize — S3 旧 stylize_l4 (qwen2.5:7b 800ms, 只在低信心区改)\n # catnify — S5 新全量改写 (qwen3:4b 3000ms, 任何 L1/L2/L3 命中都改),\n # LLM 可在输出加 <DEEPSEEK reason=\"...\"> 标记自决透传\n # 主人决策: 服务器关 MC 后 16.3GB free, 直接全量 catnify 上线.\n catty_cpu_engine_l4_mode: str = \"catnify\"\n catty_cpu_engine_l4_catnify_model: str = \"qwen3:4b\"\n catty_cpu_engine_l4_catnify_timeout_ms: int = 3000\n catty_cpu_engine_l4_catnify_max_input_tokens: int = 4000\n catty_cpu_engine_l4_catnify_temperature: float = 0.7\n catty_cpu_engine_l4_catnify_max_tokens: int = 200\n catty_cpu_engine_l4_catnify_history_turns: int = 4\n catty_cpu_engine_l4_catnify_concurrency: int = 2\n catty_cpu_engine_l4_catnify_queue_max: int = 8\n # 队列满/超时/LLM 失败时的兜底:\n # raw — 发原 CPU 候选 (主人决策默认, 信任 L1/L2/L3)\n # transparent — return False 透传 L5 (旧行为)\n catty_cpu_engine_l4_catnify_fallback_on_fail: str = \"raw\"\n # DEEPSEEK 透传限流: 每用户每小时最多透传 N 次, 防 LLM 标滥\n catty_cpu_engine_l4_catnify_deepseek_per_hour: int = 12\n # 搜索短路: 命中 web search 关键词时跳过整个 CPU 引擎让旧搜索链处理\n catty_cpu_engine_l4_catnify_search_shortcut: bool = True\n # ── S6 (主人 2026-05-29): L3 corpus 自动蒸馏 (DeepSeek 回复 → qa_corpus_live.jsonl) ──\n # 主链路 emit 完 DeepSeek 回复后异步追加, 累积 N 条触发 L3 index reload.\n # 私聊默认不采 (隐私), 仅采群聊.\n catty_cpu_engine_l3_distill_enabled: bool = True\n catty_cpu_engine_l3_distill_skip_private: bool = True\n catty_cpu_engine_l3_distill_live_corpus_path: str = (\n \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus_live.jsonl\"\n )\n catty_cpu_engine_l3_distill_rebuild_threshold: int = 50\n catty_cpu_engine_l3_distill_dedup_window: int = 1000\n catty_cpu_engine_l3_distill_min_user_len: int = 4\n catty_cpu_engine_l3_distill_max_user_len: int = 200\n catty_cpu_engine_l3_distill_min_assistant_len: int = 8\n catty_cpu_engine_l3_distill_max_assistant_len: int = 500\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "117615a4b837d27845501fe6a90b87e5e8826ac8": {"t": 1785768355.5817678, "n": 41215, "h": "50de82947da0f7de", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n # 普通会话「续聊窗口」: 笨猫回复某人后, 该用户在窗口内不用 @ 也能续聊。\n # 主人 2026-05-29: 窗口时长从 180s 砍到 25s (25s 没新消息就退出会话跟踪);\n # 续聊期间连续 catty_followup_idle_limit 次没直接提到笨猫 (非 mentioned/used_prefix) 也关窗。\n catty_followup_window_seconds: float = 25.0\n catty_followup_idle_limit: int = 3\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n \"catty_followup_window_seconds\",\n \"catty_followup_idle_limit\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "6be839ce7f4cf980d61f1c47ce836ef969e48db5": {"t": 1785768357.1195078, "n": 40868, "h": "70d09480f4096032", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 好友申请附言 / 临时会话私聊命中『包养笨猫』类援交关键词 → 自动同意好友 + 扣 100 积分\n # 进援交 sticky 窗口; 积分不够则提示签到。绕过主人手动审核, 默认关。\n catty_owner_forward_paid_auto_accept_enabled: bool = False\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "b0880fec9f1e591137ed274e85bdbe3dce6d1a7b": {"t": 1785768358.8561864, "n": 40703, "h": "4686a4ba18c029d3", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n # ── CPU 主回复引擎 (BotLibre 风格: Semantic Router + txtai) ────────────\n # 主人 2026-05-28 plan-cpu-alicebot-nlu-ai:\n # 普通闲聊走 CPU 引擎 (~零成本/<200ms), 强互动/NSFW/CPU 信心不足才打 DeepSeek.\n # 默认 enabled=False, 需 bootstrap 种子语料后再切. 关停时透传到现 keyword_reply 链路.\n catty_cpu_engine_enabled: bool = False\n catty_cpu_engine_routes_dir: str = \"src/catty_qq_ai/data/cpu_engine/routes\"\n catty_cpu_engine_corpus_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/qa_corpus.jsonl\"\n catty_cpu_engine_txtai_index_path: str = \"src/catty_qq_ai/data/cpu_engine/corpus/txtai_index\"\n # L2 Semantic Router 阈值: direct≥0.82 直答, candidate∈[0.70,0.82) 标低信心进 L4\n catty_cpu_engine_l2_threshold_direct: float = 0.82\n catty_cpu_engine_l2_threshold_candidate: float = 0.70\n # 群聊阈值上调 (避免抢话)\n catty_cpu_engine_group_threshold_bonus: float = 0.03\n # L4 Ollama 风格化: 私聊默认开, 群聊默认关 (延迟+@炸群顾虑)\n catty_cpu_engine_l4_enabled_private: bool = True\n catty_cpu_engine_l4_enabled_group: bool = False\n catty_cpu_engine_l4_timeout_ms: int = 800\n # 米雪儿语气后缀池 (Script 变量 {cat_suffix} 随机取)\n catty_cpu_engine_cat_suffixes: list[str] = Field(\n default_factory=lambda: [\"喵~\", \"喵呜\", \"ฅฅ\", \"嗷呜~\", \"爪爪\", \"贴贴\"]\n )\n # 强制走主 AI 的前缀 (绕过 CPU 引擎)\n catty_cpu_engine_force_ai_prefixes: list[str] = Field(\n default_factory=lambda: [\"#ai\", \"#aikey\", \"#refresh\"]\n )\n # Cython native 模块开关 (失败自动 fallback 纯 Python)\n catty_cpu_engine_native_enabled: bool = True\n\n # ── 积分系统 (DeepSeek 调用按 token 扣分, Ollama/CPU 免费) ─────────────\n # 主人 2026-05-28 plan-cpu-alicebot:\n # 3 起步占位 + 按真实 prompt/completion token 结算多退少补.\n # 默认 enabled=False, S3 单独验证 ledger 后再开.\n catty_credit_enabled: bool = False\n catty_credit_initial_balance: int = 100\n catty_credit_daily_signin_amount: int = 30\n catty_credit_weekly_signin_bonus: int = 50\n catty_credit_passive_recover_per_hour: int = 5\n catty_credit_passive_recover_cap: int = 100\n catty_credit_deepseek_base_cost: int = 3\n catty_credit_deepseek_per_1k_prompt: int = 2\n catty_credit_deepseek_per_1k_completion: int = 5\n catty_credit_persist_path: str = \"src/catty_qq_ai/data/credit/user_credits.json\"\n catty_credit_persist_debounce_seconds: float = 5.0\n\n # ── 强互动判定 (强制走 DeepSeek 的场景, 积分够才放行) ───────────────────\n # 主人 2026-05-28: NSFW phase>=P3 / 意图 ∈ strong_intents / 情绪强烈 / CPU 信心<阈值.\n catty_strong_cpu_confidence_threshold: float = 0.7\n catty_strong_emotion_intensity_threshold: float = 0.7\n catty_strong_intents: list[str] = Field(\n default_factory=lambda: [\"tease_cat\", \"compliment_cat\", \"表白\"]\n )\n catty_strong_nsfw_phase_threshold: int = 3\n\n # ── 每日 DeepSeek 自我进化 (审 + 改 CPU 层 template, 带 git 备份 + 自动 rollback) ──\n # 主人 2026-05-28: 全自动 DeepSeek 评审, 仅采群聊样本 (私聊不进采样池),\n # score<=2 自动退役/重写, score>=4 加权重, new_routes 灰度入 L2.\n # 默认关闭, S4 内测后再开.\n catty_evolution_enabled: bool = False\n catty_evolution_cron: str = \"0 3 * * *\"\n catty_evolution_samples_per_layer: int = 30\n catty_evolution_judge_model: str = \"deepseek-v4-flash\"\n catty_evolution_rollback_neg_feedback_pct: float = 0.2\n catty_evolution_rollback_score_decline_days: int = 3\n catty_evolution_sample_only_group: bool = True\n catty_evolution_logs_dir: str = \"src/catty_qq_ai/data/cpu_engine/evolution_logs\"\n catty_evolution_git_commit_enabled: bool = True\n\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n # 主人 2026-05-28 plan-quizzical-crane Step 3: 总结瘦身 → DeepSeek cache 友好\n # summary_max_chars: LLM 输出 summary 硬截断字数 (prompt + save 双侧守护)\n # corpus_max_tokens: 喂给 summary LLM 的语料 token 上限 (防 200+ 条 corpus 撑 8K+ 输入)\n # member_impression_max_chars: member 短画像 impression 字数 (原 140 改 70)\n catty_memory_summary_max_chars: int = 1000\n catty_memory_corpus_max_tokens: int = 2000\n catty_memory_member_impression_max_chars: int = 70\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "56de2ede75bf3fa8915b0a02f851a856bac28f37": {"t": 1785768360.358163, "n": 36901, "h": "075286dc081f39b8", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── P5.5 Lazy tool schema (description ≤30 字, properties 极简) ────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim P5.5:\n # 砍 ~3K tool schema description. OpenAI native 由 _LAZY_TOOL_SCHEMAS 直接返回,\n # Anthropic native 由 convert_openai_tool_to_anthropic 自动转 (cache 字节稳定).\n catty_tools_lazy_schema_enabled: bool = True\n # ── P5.3 每 N 轮人格 reminder ────────────────────────────────────\n # 长对话防人格漂移. P5.1 core_persona 在 cache prefix 一次 inject, 长对话末段\n # 因 LLM recency bias 可能淡化. 每 N 轮在 user msg 附近 (depth=2) 注入精简\n # (~150 token) 5 铁律 reminder, 解决漂移.\n catty_persona_reminder_enabled: bool = True\n catty_persona_reminder_every_n_turns: int = 6\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n # 主人 2026-05-28 P5.4: 每个 QQ 号一个独立 user session.\n # \"group\" = 共享 (默认旧值), \"user\" = per-user (group_id + user_id 复合 key).\n # ambient_eavesdrop/proactive/catty_mood 保持 _conversation_queue_key (群级) 看群里在场感.\n catty_group_history_scope: str = \"user\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "7e444a2d512f236ba15456e46fe0df136d3f886c": {"t": 1785768362.0508204, "n": 36022, "h": "f95f0a6d6acf0766", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n # 主人 2026-05-28 P4: 实测 sys PROTECTED 段(身份锚定)~5.6K floor, tools 3.5K,\n # 严格 5K 数学上要砍 persona → 人格漂移. 现实 budget 10K total = sys ~6K +\n # tools lazy ~1K + dynamic ~1.5K + history ~1.5K + current ~0.5K. 群聊同步松绑.\n catty_prompt_budget_private: int = 10000\n catty_prompt_budget_group: int = 7000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比. P4 调到 0.2 (10K × 0.2 = 2K history,\n # 群聊 7K × 0.2 = 1.4K), 剩余给 sys 6K + tools 1K + dynamic 1.5K + current 0.5K.\n catty_compressor_history_budget_ratio: float = 0.2\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "b4c1fad5f7bfea616074abaf05b910d0ba9c3043": {"t": 1785768364.0470598, "n": 35762, "h": "cb316ededdfa559d", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 3): monotonic anchor checkpoint ──────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim Phase 3:\n # 私聊 ≤5000 / 群聊 ≤3000 tokens. Cache-safe 设计: per-scope anchor 不动\n # → history 前缀字节稳定 → Anthropic cache prefix hit. 超 budget 时 anchor\n # 一次大跳 (剩 70% budget 留余量) → 1 次 cache miss → 之后稳定多轮.\n # NLU 失败 / disabled → 走原 history_messages, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n catty_prompt_budget_private: int = 5000\n catty_prompt_budget_group: int = 3000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比 (剩余给 system + current msg)\n # 总 5000 × 0.4 = 2000 给 history 是经验值, 主人可调\n catty_compressor_history_budget_ratio: float = 0.4\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "a8c055801478a612b0d3d1b65bc092e9b0420b2b": {"t": 1785768365.927858, "n": 35994, "h": "6ddbc6ec1ddda437", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 2): NLU 驱动的相关性裁剪 ────────────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim: 私聊 ≤5000 / 群聊 ≤3000 tokens,\n # 超出部分用 text2vec/jieba/HanLP 按相关性筛 history + user_details + summary,\n # persona 段强制保留 (_PROTECTED_IDENTIFIERS 白名单).\n # NLU 失败/超 budget 一律降级到现状逻辑, 不阻塞业务.\n #\n # 主人 2026-05-28 ⚠️ 默认 OFF:启用后 compressor 按 query 排序 history,\n # 同 scope 不同轮 query 不同 → history 字节漂移 → Anthropic cache prefix\n # 字节哈希不匹配 → cache miss (只 create 不 read). cache-friendly 重设计\n # 见 Phase 3 (monotonic compression: 同 scope 后轮只能是前轮的 superset).\n # 主人想测压缩效果时可手动开, 但会牺牲 cache hit.\n catty_prompt_compressor_enabled: bool = False\n catty_prompt_budget_private: int = 5000\n catty_prompt_budget_group: int = 3000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比 (剩余给 system + current msg)\n # 总 5000 × 0.4 = 2000 给 history 是经验值, 主人可调\n catty_compressor_history_budget_ratio: float = 0.4\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "bca58306e08a78b27f948151dbccb74649094ec6": {"t": 1785768367.3537312, "n": 35685, "h": "73da248682c0d606", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n # ── Prompt Compressor (Phase 2): NLU 驱动的相关性裁剪 ────────────────\n # 主人 2026-05-28 plan-cattyCacheFixAndPromptSlim: 私聊 ≤5000 / 群聊 ≤3000 tokens,\n # 超出部分用 text2vec/jieba/HanLP 按相关性筛 history + user_details + summary,\n # persona 段强制保留 (_PROTECTED_IDENTIFIERS 白名单).\n # NLU 失败/超 budget 一律降级到现状逻辑, 不阻塞业务.\n catty_prompt_compressor_enabled: bool = True\n catty_prompt_budget_private: int = 5000\n catty_prompt_budget_group: int = 3000\n # history 强制保留最近 N 条 (不进相关性排序). 主人决策: 配置项 + 动态扩展.\n catty_compressor_history_keep_recent_private: int = 2\n catty_compressor_history_keep_recent_group: int = 4\n # 当 NLU 判断 query 含指代/上下文依赖 (代词/省略) 时, history 额外保留 +N 条.\n catty_compressor_history_extend_when_anaphora: int = 2\n # user_details / summary 的 top-K 选取数\n catty_compressor_user_details_top_k: int = 5\n catty_compressor_summary_top_paragraphs: int = 3\n # cosine 相似度 + 时间衰减 → 综合 score 中, recency 占比 (0=纯语义, 1=纯时序)\n catty_compressor_recency_weight: float = 0.3\n # history 段在总 budget 里的占比 (剩余给 system + current msg)\n # 总 5000 × 0.4 = 2000 给 history 是经验值, 主人可调\n catty_compressor_history_budget_ratio: float = 0.4\n # ── Cache 诊断日志 (Phase 1.4): 默认关闭, 主人按需打开 ────────────────\n # 打开后会往 logs/cache_debug.log 打 cache_control 标位 + system_blocks 字节诊断.\n catty_cache_diag_enabled: bool = False\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "42f2f2d444b6c4abc2ad74b2e17baabac4efa9c5": {"t": 1785768368.914506, "n": 34355, "h": "bbb4cbb863878f86", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # 主人 2026-05-28 Phase 1.4: cache 诊断 INFO 日志总开关.\n # True (默认) → prefix_hash / cache_diff / actual_kwargs / cache_prefix_dump / raw_body_dump\n # 走 INFO level 留生产 (爪爪日后回查).\n # False → 降到 DEBUG level, 默认 logger 阈值看不到, 日志体积降低.\n catty_cache_diag_enabled: bool = True\n # 主人 2026-05-28 Phase 1.3: native 入口扫到 messages 含 role=system 时打 WARNING.\n # 守卫 _post_boundary 等动态段意外以 system role 漏出, 帮助定位 cache miss 元凶.\n # 不影响生产 (会 sweep 到 system_blocks), 仅诊断辅助.\n catty_cache_warn_on_inline_system: bool = True\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "dd35bf5d8963cf7c74a5e00d442945910f00efea": {"t": 1785768370.3437204, "n": 33820, "h": "05164104fe4a9322", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # 主人 2026-05-28 Phase 1.2: 第二轮请求注入 extra_body.diagnostics.previous_message_id\n # 用来跟踪 cache miss 原因. NewAPI / 中转 relay 不识别 diagnostics 字段时第二轮会 500.\n # 默认 OFF, 只有直连 anthropic.com 时才打开.\n catty_cache_diag_previous_message_id_enabled: bool = False\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "21151b9f81dba08392a700d19f6079d5c579cef4": {"t": 1785768371.7032619, "n": 33563, "h": "7b55b8f848994448", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 5 # 主人 2026-05-28 C16-10: 20→5 砍 user content\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "0fe0032337590f45bf958e3be1661b291a3b5384": {"t": 1785768373.143852, "n": 33519, "h": "a2fab7a271c672ef", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 3 # 主人 2026-05-28 C15-6: 6→3 (6 条), 强模型 + 8.4K cache 锁人格, history 给最近延续就够\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "0d7df1e0ad5b1e219b1cb58dfe9aad8675628bea": {"t": 1785768374.5666847, "n": 33496, "h": "6c5062c6d58490a2", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 6 # 主人 2026-05-28 C14: 16→6 (12 条), 主人原话\"8-12 条刚好\"\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "5525fc6306e5da28b2a2bee4a7d9f92c6e5a6554": {"t": 1785768375.9433277, "n": 33447, "h": "991799988f04be6d", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n # 主人 2026-05-28 v2: per-topic threshold 覆盖. config.json 可填:\n # \"catty_text2vec_topic_threshold_overrides\": {\"food\": 0.62, \"tech\": 0.48}\n # 留空 dict 走 prototypes._PER_TOPIC_THRESHOLDS 默认.\n catty_text2vec_topic_threshold_overrides: dict[str, float] = Field(default_factory=dict)\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "d034b279118d28abab6e0295dcc4c5ac99f5662d": {"t": 1785768377.4876883, "n": 33158, "h": "c65802575ab78699", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 v2: 三库已部署且 v1 验证通过 (A/B 26/30), 默认 flip 到 True.\n # 失败 graceful fallback 到 legacy regex 路径 (装包问题不会让 bot 起不来).\n catty_use_jieba: bool = True\n catty_use_text2vec: bool = True\n catty_use_hanlp: bool = True\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 已部署验证, 默认 True. 装 optimum/onnxruntime 失败 → 自动 fallback torch.\n catty_text2vec_use_onnx: bool = True\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "7c60ccc5c71dd16940b95babbe99207b9bf8f3ad": {"t": 1785768378.8644285, "n": 33334, "h": "fe71a43c1b69ba54", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n # 主人 2026-05-28: 30 → 60,「翻翻笔记」式 placeholder 触发别太急,让 sonnet 多想会儿\n catty_slow_reply_placeholder_seconds: float = 60.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 确认: 加 encoder-only 本地 NLU 增强层, 不生成不算 LLM.\n # 每个 toggle 独立, 失败 graceful fallback 到 legacy regex 路径.\n # Commit 1 上 jieba; Commit 2 上 text2vec; Commit 3 上 hanlp.\n # 默认全 False, 每 commit 部署+验证后主人手动 flip 到 True.\n catty_use_jieba: bool = False\n catty_use_text2vec: bool = False\n catty_use_hanlp: bool = False\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 开后 text2vec_engine 优先 ONNX, 失败自动 fallback torch. 默认 False.\n # 需要 optimum + onnxruntime 装好 (pip install \"optimum[onnxruntime]\").\n catty_text2vec_use_onnx: bool = False\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "fa2d27a6f6322bf0ad9d8fad6eda9bce6e059a20": {"t": 1785768380.3240774, "n": 33265, "h": "e7dbfc67a8246b58", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n catty_slow_reply_placeholder_seconds: float = 30.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28 C7: cache TTL 改回 5min (默认 Anthropic 标准).\n # 价格: 5min write 1.25x, 1h write 2x, cache read 都 0.1x.\n # 改回 5min 原因: 主人观察到 1h + dynamic content 一起 cache 后, Claude 会触发 safety\n # refusal (\"我需要在这里停下来\"). 短 TTL 让 cache prefix 不会跨多个 user session 持久化.\n catty_cache_ttl: str = \"5min\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 确认: 加 encoder-only 本地 NLU 增强层, 不生成不算 LLM.\n # 每个 toggle 独立, 失败 graceful fallback 到 legacy regex 路径.\n # Commit 1 上 jieba; Commit 2 上 text2vec; Commit 3 上 hanlp.\n # 默认全 False, 每 commit 部署+验证后主人手动 flip 到 True.\n catty_use_jieba: bool = False\n catty_use_text2vec: bool = False\n catty_use_hanlp: bool = False\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 开后 text2vec_engine 优先 ONNX, 失败自动 fallback torch. 默认 False.\n # 需要 optimum + onnxruntime 装好 (pip install \"optimum[onnxruntime]\").\n catty_text2vec_use_onnx: bool = False\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "0ce0a1f220a92707e03254be1833bd2da3db23bd": {"t": 1785768381.7532525, "n": 33198, "h": "f52f3a03706d7910", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n # 专用 HTTP/SOCKS proxy 走 NovelAI 上行 — 远端国内服务器到 image.novelai.net\n # 真实 IP 被墙时填这个, 其他路径(napcat / chat completion)不受影响。\n # 留空 = 直连。支持 http://user:pass@host:port 和 socks5://user:pass@host:port。\n catty_imagegen_nai_http_proxy: str = \"\"\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n catty_slow_reply_placeholder_seconds: float = 30.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28: cache TTL — 默认 1h, CC 同款.\n # 价格: 5min cache write 1.25x, 1h cache write 2x, cache read 都 0.1x. 长会话 (5min+) 收益大.\n # 改成 \"5min\" 可回退到原默认; \"1h\" 启用 extended-cache-ttl-2025-04-11 beta.\n catty_cache_ttl: str = \"1h\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 确认: 加 encoder-only 本地 NLU 增强层, 不生成不算 LLM.\n # 每个 toggle 独立, 失败 graceful fallback 到 legacy regex 路径.\n # Commit 1 上 jieba; Commit 2 上 text2vec; Commit 3 上 hanlp.\n # 默认全 False, 每 commit 部署+验证后主人手动 flip 到 True.\n catty_use_jieba: bool = False\n catty_use_text2vec: bool = False\n catty_use_hanlp: bool = False\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 开后 text2vec_engine 优先 ONNX, 失败自动 fallback torch. 默认 False.\n # 需要 optimum + onnxruntime 装好 (pip install \"optimum[onnxruntime]\").\n catty_text2vec_use_onnx: bool = False\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "677177a2116dce2771b26d594fad74d04844382f": {"t": 1785768383.244543, "n": 32954, "h": "b49d283efc390c67", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n catty_slow_reply_placeholder_seconds: float = 30.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # 主人 2026-05-28: cache TTL — 默认 1h, CC 同款.\n # 价格: 5min cache write 1.25x, 1h cache write 2x, cache read 都 0.1x. 长会话 (5min+) 收益大.\n # 改成 \"5min\" 可回退到原默认; \"1h\" 启用 extended-cache-ttl-2025-04-11 beta.\n catty_cache_ttl: str = \"1h\"\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 确认: 加 encoder-only 本地 NLU 增强层, 不生成不算 LLM.\n # 每个 toggle 独立, 失败 graceful fallback 到 legacy regex 路径.\n # Commit 1 上 jieba; Commit 2 上 text2vec; Commit 3 上 hanlp.\n # 默认全 False, 每 commit 部署+验证后主人手动 flip 到 True.\n catty_use_jieba: bool = False\n catty_use_text2vec: bool = False\n catty_use_hanlp: bool = False\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 开后 text2vec_engine 优先 ONNX, 失败自动 fallback torch. 默认 False.\n # 需要 optimum + onnxruntime 装好 (pip install \"optimum[onnxruntime]\").\n catty_text2vec_use_onnx: bool = False\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}, "8531323e917467da59926472f931ad82e65af88b": {"t": 1785768385.6293368, "n": 32717, "h": "845e939227ce2ebc", "c": "import json\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _split_text(value: str) -> list[str]:\n return [item.strip() for item in re.split(r\"[,,;;\\n]+\", value) if item.strip()]\n\n\ndef _parse_json_object(value: str) -> dict[str, Any]:\n raw = value.strip()\n if not raw:\n return {}\n loaded = json.loads(raw)\n if not isinstance(loaded, dict):\n raise ValueError(\"value must be a JSON object\")\n return loaded\n\n\nclass KeywordReplyRule(BaseModel):\n keywords: list[str] = Field(default_factory=list)\n reply: str = \"\"\n enabled: bool = True\n # 同一会话(以群为单位)在该秒数内不再重复回此规则;0 表示无冷却。\n cooldown_seconds: float = 0.0\n\n @field_validator(\"keywords\", mode=\"before\")\n @classmethod\n def parse_keywords(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"keyword reply keywords JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n\nclass Config(BaseModel):\n catty_openai_base_url: str = \"https://api.openai.com/v1\"\n catty_openai_api_key: str = \"\"\n catty_openai_model: str = \"gpt-4o-mini\"\n catty_openai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_openai_extra_body: dict[str, Any] = Field(default_factory=dict)\n\n # 主 AI 主动生图(catty_imagegen tool)— 走 OpenAI Image API。\n # base_url/api_key 留空则复用上面 catty_openai_* 同一通道(codex/gpt-5.5 同 host 都行)。\n # 默认 low quality 1024x1024,省钱;主 AI 可在 args 里覆盖。\n catty_imagegen_enabled: bool = True\n catty_imagegen_base_url: str = \"\"\n catty_imagegen_api_key: str = \"\"\n catty_imagegen_model: str = \"gpt-image-2\"\n catty_imagegen_default_size: str = \"1024x1024\"\n catty_imagegen_default_quality: str = \"low\"\n catty_imagegen_default_format: str = \"png\"\n catty_imagegen_cooldown_seconds: int = 180\n # 生图慢:gpt-image-2 high quality 大图可能要 200s+,默认 300s 给足。\n catty_imagegen_timeout_seconds: float = 300.0\n catty_imagegen_max_chars: int = 2000\n # 写本地缓存目录(napcat 走本地文件比 base64 inline 大图稳)。相对路径 = bot cwd\n catty_imagegen_cache_dir: str = \"pictures/imagegen_cache\"\n # 缓存目录最多保留 N 张,LRU(超出删最老)\n catty_imagegen_cache_max_files: int = 200\n # 把 imagegen 请求的 https:// 强制改成 http:// 绕过上游 CF 反代 100s Origin Timeout 硬限。\n # 主人贴的账单实测 OpenAI gpt-image-2 经常 150-200s 才完成,HTTPS 走 CF 边缘 124s 就 524,\n # HTTP 直连 origin 没有这个限制。chat_completion 短不受影响,保持 HTTPS。\n catty_imagegen_force_http_scheme: bool = True\n\n # ── NovelAI 路径(provider=\"nai\") ──\n # 主 AI 在 catty_imagegen tool 里选 provider=\"nai\" 时走这条; 二次元/动漫/角色立绘适用。\n # 三个标准尺寸: portrait 832x1216 / landscape 1216x832 / square 1024x1024\n # Opus tier3 订阅在这三档 + steps<=28 + n_samples=1 是免费的(Anlas=0)。\n catty_imagegen_nai_enabled: bool = False\n catty_imagegen_nai_token: str = \"\"\n catty_imagegen_nai_model: str = \"nai-diffusion-4-5-full\"\n catty_imagegen_nai_default_aspect: str = \"portrait\" # portrait / landscape / square\n catty_imagegen_nai_steps: int = 28\n catty_imagegen_nai_scale: float = 5.0\n catty_imagegen_nai_sampler: str = \"k_euler_ancestral\"\n catty_imagegen_nai_noise_schedule: str = \"karras\"\n catty_imagegen_nai_default_negative: str = (\n \"lowres, worst quality, low quality, bad anatomy, bad hands, \"\n \"missing fingers, extra fingers, watermark, signature, jpeg artifacts\"\n )\n catty_imagegen_nai_timeout_seconds: float = 180.0\n # 基础 5 积分,多 1 Anlas +3 积分(三个标准尺寸 + 默认 28 steps 时 Anlas=0,只扣 5)\n catty_imagegen_nai_base_points: int = 5\n catty_imagegen_nai_points_per_anlas: int = 3\n\n # 慢请求 placeholder:主回复 chat_completion 进入后超过该秒数没回,先 send 一句轻量占位\n # 避免用户以为 bot 卡死了/被忽略了。0 或负数 = 禁用。\n catty_slow_reply_placeholder_seconds: float = 30.0\n # 占位消息后台 task 异常时静默(主回复链路不能被打断)\n catty_slow_reply_placeholder_max_messages: int = 1\n\n # ST 风 random encounter — 每条 reply 触发『本轮主动小开场』hint 的概率(0-1)。\n # 不是 push 主动消息(catty 是 reactive), 而是当用户 reply 时偶尔笨猫会冒一句\n # 自己的小事开场,让对话不再 100% 被动响应。0 = 禁用,>0.10 会很吵不建议。\n catty_random_encounter_chance: float = 0.03\n\n # IDE 多 tab 风格的会话排队:每群最多 N 个并发回复(默认 3),不同用户能并行;\n # 同一用户在同群仍串行(防同人乱序爆消息)。0 或负数 = 退化回老的一群一把大锁。\n catty_reply_group_concurrency: int = 3\n\n # 主 AI 不可用时的本地 fallback(默认指向项目内 Ollama qwen2.5:7b)\n catty_ai_fallback_enabled: bool = False\n catty_ai_fallback_base_url: str = \"\"\n catty_ai_fallback_api_key: str = \"\"\n catty_ai_fallback_model: str = \"\"\n catty_ai_fallback_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_ai_fallback_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_ai_fallback_temperature: float | None = None\n catty_ai_fallback_max_tokens: int | None = 4096\n catty_ai_fallback_request_timeout: float | None = 180.0\n # 触发 fallback 后,主 AI 在该秒数内直接走本地不再重试云\n catty_ai_fallback_cooldown_seconds: float = 300.0\n # MC 有玩家在线时禁止本地 fallback 推理(保护游戏流畅度)\n catty_ai_fallback_mc_gate_enabled: bool = True\n catty_ai_fallback_mc_server_host: str = \"localhost\"\n catty_ai_fallback_mc_server_port: int = 26843\n catty_ai_fallback_mc_ping_timeout_seconds: float = 3.0\n # 兜底模型已经把人格烧进 Modelfile SYSTEM 时,剥离 system role 消息避免 KV cache 失效\n # 留空(默认)时:若 model 以 \"catty-\" 开头自动启用,否则不剥离\n catty_ai_fallback_strip_system_messages: bool = False\n\n catty_audit_ai_base_url: str = \"\"\n catty_audit_ai_api_key: str = \"\"\n catty_audit_ai_model: str = \"\"\n catty_audit_ai_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_audit_ai_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_audit_ai_temperature: float | None = 0.1\n catty_audit_ai_max_tokens: int | None = 320\n catty_audit_ai_request_timeout: float | None = 60.0\n\n catty_vision_base_url: str = \"\"\n catty_vision_api_key: str = \"\"\n catty_vision_model: str = \"\"\n catty_vision_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_vision_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_vision_prompt: str = \"请识别图片内容,提取和聊天回复相关的信息。不要评价图片是否安全,不要添加 emoji;如果有文字请尽量转写。\"\n catty_vision_temperature: float | None = 0.2\n catty_vision_max_tokens: int | None = 800\n catty_vision_request_timeout: float | None = None\n # vision 走异步:消息一进来就 fire-and-forget 启 task,主回复链路最多短等\n # catty_vision_inline_max_wait_seconds 秒;等不到就不带 vision 描述直接回,\n # 后台 task 跑完写 memory_store,下一轮自动复用。\n # 注:默认值要大于 vision 模型的平均响应时间(8-12s 常见),否则主回复总赶在\n # vision 完成前就走,看起来像\"猫猫不识图\"。配合 reply_queue_max_wait_seconds\n # 排队雪崩防护使用,排队消息超 25s 才丢弃,所以这里给 15s 是安全的。\n catty_vision_async_enabled: bool = True\n catty_vision_inline_max_wait_seconds: float = 15.0\n\n catty_filter_enabled: bool = True\n catty_filter_base_url: str = \"\"\n catty_filter_api_key: str = \"\"\n catty_filter_model: str = \"\"\n catty_filter_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_filter_extra_body: dict[str, Any] = Field(default_factory=dict)\n catty_filter_temperature: float | None = 0.0\n catty_filter_max_tokens: int | None = 64\n catty_filter_request_timeout: float | None = 10.0\n # NSFW deep 路径 (stage 8/9/10) 专用 model. 空时 fallback 到 catty_filter_model.\n # **不 hardcode 默认值**, 主人在 config.json 里设.\n catty_nsfw_spark_model: str = \"\"\n # NSFW spark 独立 base_url / api_key (主人 2026-05-27 选项 2):\n # 空时 fallback 到 catty_filter_base_url / catty_filter_api_key (跟 mood classifier 同站).\n # 配上时 nsfw_spark_model 走独立 endpoint (例如 deepseek 官方), mood classifier 仍走 filter.\n catty_nsfw_spark_base_url: str = \"\"\n catty_nsfw_spark_api_key: str = \"\"\n # placeholder / 签到 caption 短回复路径专用 model (主人原话: 不要 spark, 走 5.3-codex).\n # chat_completion_codex_instant 优先级: model_override > codex_instant_model > nsfw_spark_model > filter_model.\n # 空时 fallback 到 nsfw_spark_model 或 filter_model. **不 hardcode 默认值**, 在 config.json 里设.\n catty_codex_instant_model: str = \"\"\n # 累计软拒达 threshold 后自动切的 fallback model (默认空 → 用 catty_filter_model = spark).\n # 例: 主 model = gpt-5.5, fallback = gpt-5.3-codex-spark.\n catty_nsfw_fallback_model: str = \"\"\n catty_nsfw_softrefuse_threshold: int = 2\n # Anthropic Prompt Caching (ST PR #3085 移植) — 显式注入 cache_control: ephemeral.\n # OpenAI native 是 implicit caching 不需要; Claude / 中间人走 Claude 协议时需要显式标记.\n # 启用后 spark + 主路径都注入 4 个 breakpoint (system tail + messages depth 2/4 + tools tail),\n # log cache_read_input_tokens / cache_creation_input_tokens 监测命中率.\n # 主人 2026-05-26 确认 cache 对 spark NSFW 无副作用, 默认 True.\n # 中间人若不支持会忽略 unknown field 不报错; Claude/Anthropic 协议命中真实 cache.\n catty_prompt_cache_enabled: bool = True\n # cache_control 注入的 depth (从 messages 末尾倒数第 N 处 role 切换). ST 默认 2.\n catty_prompt_cache_depth: int = 2\n # ── Anthropic native /v1/messages 路径 (主人 2026-05-28: NewAPI SG relay\n # pass_through_body_enabled=true 后, 中转层字节级透传 body 含 cache_control /\n # context_management / metadata. 切换该开关后 catty 走 anthropic SDK 发原生请求,\n # 享受 server-side compaction (compact-2026-01-12) — input>150K 自动 server 端摘要.\n # OAuth MAX 下 cache_creation/read 静默 0 不是 bug; 未来换付费 sk-ant-api03- key\n # 自动享 90% cache 折扣.\n catty_anthropic_native_enabled: bool = False\n # server-side compaction (compact-2026-01-12 beta). 自动在 input>trigger_tokens 时\n # server 端跑摘要并在 response.content 里插 compaction block + applied_edits[] 标记.\n catty_compaction_enabled: bool = False\n catty_compaction_trigger_tokens: int = 150000 # Anthropic 默认 150K\n catty_filter_group_batch_messages: int = 200\n catty_filter_group_batch_seconds: float = 1200.0\n # ── Local NLU enrichment (jieba / text2vec / HanLP) ─────────────────\n # 主人 2026-05-28 确认: 加 encoder-only 本地 NLU 增强层, 不生成不算 LLM.\n # 每个 toggle 独立, 失败 graceful fallback 到 legacy regex 路径.\n # Commit 1 上 jieba; Commit 2 上 text2vec; Commit 3 上 hanlp.\n # 默认全 False, 每 commit 部署+验证后主人手动 flip 到 True.\n catty_use_jieba: bool = False\n catty_use_text2vec: bool = False\n catty_use_hanlp: bool = False\n # 主人 2026-05-28 phase 6: ONNX runtime fast path (单 embed 2-3ms vs torch 50-85ms).\n # 开后 text2vec_engine 优先 ONNX, 失败自动 fallback torch. 默认 False.\n # 需要 optimum + onnxruntime 装好 (pip install \"optimum[onnxruntime]\").\n catty_text2vec_use_onnx: bool = False\n # 主人 2026-05-28 phase 5: 换 BGE small (~95MB, 512 dim) — 中文 STS 比\n # text2vec-base-chinese 公开榜准 2-3pp. 模型大小同级, 加载速度类似.\n # 切换后 prototype hash 自动 invalidate, 启动重 build ~30s.\n # 老主人 config 已配 shibing624 时此默认不生效, 想换需手动改 config.json.\n catty_text2vec_model_name: str = \"BAAI/bge-small-zh-v1.5\"\n catty_hanlp_pipeline: str = \"FINE_ELECTRA_SMALL_ZH\"\n catty_text2vec_topic_threshold: float = 0.55\n catty_text2vec_emotion_threshold: float = 0.45\n catty_text2vec_trend_threshold: float = 0.50\n catty_nlu_cache_dir: str = \"src/catty_qq_ai/data/nlu_cache\"\n catty_nlu_warmup_on_startup: bool = True\n # 大陆环境 HuggingFace 镜像 (空时不强制改 HF_ENDPOINT, 用环境变量原值)\n catty_nlu_hf_endpoint: str = \"https://hf-mirror.com\"\n catty_filter_anger_enabled: bool = False # 主人:每条群消息都喂 spark 判耐心太烧\n catty_filter_anger_warn_threshold: int = 60\n catty_filter_anger_mute_threshold: int = 100\n catty_filter_anger_cooldown_seconds: int = 3600\n catty_local_critic_enabled: bool = False\n catty_local_critic_mode: str = \"reply_gate_only\"\n catty_local_critic_base_url: str = \"http://127.0.0.1:11434/v1\"\n catty_local_critic_api_key: str = \"ollama\"\n catty_local_critic_model: str = \"qwen2.5:1.5b\"\n catty_local_critic_extra_headers: dict[str, str] = Field(default_factory=dict)\n catty_local_critic_extra_body: dict[str, Any] = Field(default_factory=lambda: {\"think\": False})\n catty_local_critic_temperature: float | None = None\n catty_local_critic_max_tokens: int | None = 16\n catty_local_critic_request_timeout: float | None = 4.0\n catty_local_critic_rewrite_when_score_below: int = 0\n # ── [DEPRECATED 2026-05-27 reply gate kill] ──────────────────────────\n # reply gate 整个停了, 下面这些字段已不被读取. 保留 schema 兼容旧 config.json,\n # 不要在新逻辑里读它们. 真正决定要不要回的逻辑在 __init__.py 的 _rule 里 (本地确定性判断).\n catty_local_critic_reply_gate_enabled: bool = False # 主人:指向猫猫的全交主 AI 自己判断\n catty_local_critic_reply_gate_min_confidence: int = 55\n catty_local_critic_reply_gate_examples: int = 0\n catty_local_critic_reply_gate_max_tokens: int | None = 16\n catty_local_critic_reply_gate_request_timeout: float | None = 4.0\n catty_local_critic_reply_gate_user_message_chars: int = 120\n catty_local_critic_reply_gate_plain_text_chars: int = 60\n catty_local_critic_reply_gate_context_chars: int = 80\n catty_local_critic_warmup_enabled: bool = True\n catty_local_critic_warmup_keep_alive: str = \"30m\"\n catty_local_critic_warmup_interval_seconds: float = 300.0\n catty_local_critic_warmup_request_timeout: float = 60.0\n catty_local_critic_force_direct_reply: bool = True\n catty_local_critic_collect_training_samples: bool = True\n catty_local_critic_training_samples_path: str = \"local_critic_samples.jsonl\"\n catty_local_training_collect_assistant_samples: bool = True\n catty_local_training_assistant_samples_path: str = \"training/assistant_reply_samples.jsonl\"\n catty_web_search_enabled: bool = True\n catty_web_search_cooldown_seconds: int = 60\n catty_web_search_max_results: int = 5\n catty_web_search_request_timeout: float | None = 10.0\n catty_web_search_engines: list[str] = Field(default_factory=lambda: [\"google\", \"bing\"])\n catty_nsfw_search_enabled: bool = True\n catty_nsfw_search_max_results: int = 4\n catty_nsfw_search_request_timeout: float | None = 15.0\n catty_nsfw_search_cooldown_seconds: int = 30\n catty_nsfw_image_send_count: int = 2\n catty_nsfw_pixiv_cookie: str = \"\"\n catty_nsfw_pixiv_image_size: str = \"regular\"\n\n # 反向搜图(catty_image_search tool):SauceNAO / trace.moe / ascii2d / iqdb\n # 多引擎并发,问出「谁画的/什么番/角色/出处」。SauceNAO 需 API key (saucenao.com 注册免费),\n # 没填会自动跳过 saucenao 走其它引擎。trace.moe / ascii2d / iqdb 无需 key。\n catty_image_search_enabled: bool = True\n catty_image_search_saucenao_api_key: str = \"\"\n catty_image_search_cooldown_seconds: int = 60\n catty_image_search_max_results: int = 5\n catty_image_search_request_timeout: float | None = 15.0\n\n catty_owner_qq: int = 0\n catty_owner_forward_enabled: bool = False\n catty_owner_forward_private_messages: bool = True\n catty_owner_forward_block_ai_reply: bool = True\n # 猫猫(笨猫人格本身)各平台社交账号:被群友问起\"你的 steam\"/\"猫猫 steam\"\n # 或聊到某游戏想给出猫猫自己对应平台账号时引用,空字符串表示猫猫在那个平台没账号。\n catty_social_steam: str = \"\"\n catty_turtle_soup_cooldown_seconds: int = 300\n\n catty_system_prompt: str = \"你是一个接入 QQ 的中文 AI 助手,回答要友好、简洁、可靠。\"\n catty_trigger_prefixes: list[str] = Field(default_factory=lambda: [\"ai\", \"AI\", \"猫猫\"])\n catty_enable_private: bool = True\n catty_enable_group: bool = True\n catty_private_require_prefix: bool = False\n catty_group_require_mention_or_prefix: bool = True\n catty_group_history_scope: str = \"group\"\n catty_history_turns: int = 16\n catty_session_cache_persistence_enabled: bool = True\n catty_session_cache_dir: str = \"sessions\"\n catty_session_cache_max_sessions: int = 200\n catty_session_cache_save_debounce_seconds: float = 2.0\n # bot 主进程 CPU affinity (Windows). 0 = 不绑核 (默认 OS 自由调度)\n # 6 核机器:1 = 核0 only,把 Ollama 留给核1-5\n catty_cpu_affinity_mask: int = 0\n catty_directed_keywords: list[str] = Field(\n default_factory=lambda: [\"你\", \"猫猫\", \"猫娘\", \"看看\", \"帮我看看\", \"这张图\", \"这个图\", \"图片\", \"图里\", \"评价一下\", \"怎么回事\"]\n )\n catty_keyword_replies: list[KeywordReplyRule] = Field(default_factory=list)\n catty_soft_directed_reply_probability: float = 0.65\n catty_direct_address_reply_probability: float = 0.9\n catty_image_response_enabled: bool = True\n catty_image_vision_enabled: bool = True\n # 同会话排队雪崩防护:消息在锁队列里等待超这个秒数就放弃当前消息,\n # 避免视觉/AI 卡顿后积压消息一起爆出来。\n catty_reply_queue_max_wait_seconds: float = 25.0\n catty_emoji_enabled: bool = True\n catty_emoji_dir: str = \"emojis\"\n catty_emoji_download_dir: str = \"emojis/downloaded\"\n catty_emoji_manifest_path: str = \"emojis/manifest.json\"\n catty_emoji_interest_threshold: int = 60\n catty_emoji_save_interest_threshold: int = 85\n catty_emoji_max_candidates: int = 8\n catty_emoji_reply_enabled: bool = True\n catty_emoji_reply_probability: float = 0.85\n catty_emoji_auto_fallback_enabled: bool = False\n catty_emoji_diversity_enabled: bool = True\n catty_emoji_diversity_recent_window: int = 8\n catty_emoji_diversity_candidate_pool: int = 6\n catty_legs_enabled: bool = True\n catty_legs_pictures_dir: str = \"pictures\"\n catty_legs_cooldown_seconds: float = 3.0\n # 戳一戳:避免被刷屏。同一用户在同一会话内的最小回复间隔与回应概率\n catty_poke_cooldown_seconds: float = 12.0\n catty_poke_reply_probability: float = 1.0\n catty_memory_enabled: bool = True\n catty_memory_path: str = \"memory.json\"\n catty_memory_group_storage_dir: str = \"\"\n catty_memory_user_storage_dir: str = \"\"\n catty_memory_max_known_members: int = 20\n catty_memory_special_group_ids: set[int] = Field(default_factory=set)\n catty_special_care_user_ids: set[int] = Field(default_factory=set)\n catty_group_special_care_user_ids: dict[str, set[int]] = Field(default_factory=dict)\n catty_special_care_cooldown_seconds: int = 90\n catty_special_care_response_window_minutes: float = 30.0\n catty_memory_summary_interval_minutes: int = 30\n catty_memory_max_corpus_messages: int = 800\n catty_memory_private_summary_messages: int = 500\n catty_memory_member_mention_threshold: int = 20\n catty_memory_reply_boost_enabled: bool = True\n catty_memory_reply_boost_min_corpus_messages: int = 80\n catty_memory_reply_boost_probability_bonus: float = 0.15\n catty_memory_reply_boost_max_probability: float = 0.95\n # 记忆落盘 debounce:把高频群消息触发的 _save() 合并成最多每 N 秒一次实际写盘。\n catty_memory_save_debounce_seconds: float = 2.0\n catty_special_group_active_window_enabled: bool = False\n catty_special_group_active_minutes_per_hour: int = 10\n catty_group_titles: dict[str, str] = Field(default_factory=dict)\n catty_user_titles: dict[str, str] = Field(default_factory=dict)\n catty_group_user_titles: dict[str, dict[str, str]] = Field(default_factory=dict)\n\n catty_game_context_star_resonance_group_ids: set[int] = Field(default_factory=set)\n catty_game_context_strinova_group_ids: set[int] = Field(default_factory=set)\n\n catty_proactive_enabled: bool = True\n catty_proactive_max_daily_per_group: int = 5\n catty_proactive_check_interval_seconds: float = 300.0\n catty_proactive_min_interval_minutes: float = 120.0\n catty_proactive_response_window_minutes: float = 30.0\n catty_proactive_recent_messages: int = 40\n # 活跃门槛:最近 active_window_minutes 内群友消息条数 < active_min_messages 时不主动冒泡。\n # 设为 0 即关闭该门槛(回到旧的\"按目标和间隔无脑冒泡\"行为)。\n catty_proactive_active_window_minutes: float = 15.0\n catty_proactive_active_min_messages: int = 5\n\n # ── 游戏记忆库(独立于 user/group 记忆) ──────────────────────────────\n # AI 调 catty_web_search 在游戏群拿到结果时自动 sink;AI 也可以主动用\n # catty_game_remember 写入。catty_game_recall 是双向查询接口。\n # 文件结构:每个游戏一个 JSON,memory_games/game_{name}.json。\n catty_memory_game_storage_dir: str = \"memory_games\"\n catty_memory_max_game_facts: int = 200 # 每个游戏最多保留多少条事实(超出按时间淘汰最旧)\n catty_memory_max_game_fact_chars: int = 360 # 单条事实文本上限\n # 周期性摘要:每个游戏 facts 数 >= min_facts 且距上次摘要 >= interval_minutes 时触发 LLM 压缩,\n # 摘要后只保留最近 keep_recent_facts 条原始事实,其它压缩进 summary。\n catty_memory_game_summary_min_facts: int = 60\n catty_memory_game_summary_interval_minutes: float = 360.0 # 6 小时\n catty_memory_game_keep_recent_facts: int = 20\n # 单游戏 JSON 文件超过此字节数,强制触发一次 LLM 压缩(走和 due_games_for_summary 同一通道),\n # 避免长期积累让 game_<name>.json 无限膨胀。默认 200KB。\n catty_memory_game_size_compress_threshold_bytes: int = 200_000\n\n # ── 主 AI 工具调用(OpenAI function calling 协议) ──────────────────\n # 主回复时给云端模型挂 catty_recall / catty_user_profile / catty_mc_status 三套 tool schema,\n # 由模型自行判断要不要查询。默认常驻挂载,让上游 prompt cache 命中 tools 部分。\n # 上游返回 400 或 tool 调度异常时会自动降级一次纯文本回复。\n catty_tools_enabled: bool = True\n # 本地解析层(在 system prompt 注入前运行) — 出问题时可临时关掉某层。\n # 合法值: slang / pulse / intent / entity / hints\n # 留空表示全部启用(默认推荐)。例如 [\"hints\"] 表示只关 hints,其它都开。\n catty_parsing_layers_disabled: list[str] = Field(default_factory=list)\n # SillyTavern 风 PromptManager 配置(只影响 ST 风新模块: daily_life / world_info / story_arc)\n # 列表里出现的 identifier 按 (i+1)*100 排序;不出现的保持模块默认 order。\n # 合法值: catty_daily_life / catty_world_info / catty_story_arc\n # 留空 → 用各模块默认 order\n catty_prompt_order: list[str] = Field(default_factory=list)\n # 单独关闭某段 ST 风 prompt。同上 identifier。\n catty_prompts_disabled: list[str] = Field(default_factory=list)\n # 单次主回复最多允许的 tool 调用轮次(防止模型反复循环调 tool)\n catty_tools_max_rounds: int = 3\n # 每个 tool 结果的 in-process LRU TTL(秒);0 表示不缓存\n catty_tools_cache_ttl_seconds: float = 60.0\n # 单次 tool 调用上限(每轮多个 tool_calls 也算)\n catty_tools_max_calls_per_round: int = 3\n # 私聊场景下额外排除哪些 tool;群聊默认全开\n catty_tools_disabled_in_private: list[str] = Field(default_factory=lambda: [\"catty_user_profile\"])\n\n catty_temperature: float | None = 0.7\n # 拉高 max_tokens 默认值——gpt-5.5 调 catty_imagegen 时 tool_calls 和最终回复\n # 都要装下,12800 偶尔不够长 prompt 转写 + 多轮 + 短评。改 32000 给足缓冲。\n catty_max_tokens: int | None = 32000\n catty_request_timeout: float = 60.0\n catty_reply_max_chars: int = 1800\n catty_reply_human_split_enabled: bool = True\n catty_reply_human_split_probability: float = 0.35\n catty_reply_human_split_min_chars: int = 48\n catty_reply_human_split_max_chunks: int = 4\n catty_reply_human_split_delay_seconds: float = 0.8\n catty_reply_mix_emoji_with_text: bool = True\n catty_reply_quote_enabled: bool = True\n catty_reply_quote_private_enabled: bool = False\n catty_reply_self_check_enabled: bool = True\n catty_reply_style_examples_enabled: bool = True\n catty_expression_repeat_enabled: bool = True\n catty_expression_repeat_threshold: int = 2\n catty_expression_repeat_window_seconds: float = 20.0\n catty_expression_repeat_include_images: bool = True\n catty_expression_repeat_include_text: bool = True\n\n catty_allowed_user_ids: set[int] = Field(default_factory=set)\n catty_allowed_group_ids: set[int] = Field(default_factory=set)\n # bot-loop 防护三层:\n # 1) 硬黑名单 QQ 号(如 Q 群管家明确加进来)\n # 2) QQ 协议层机器人标记(NapCat / OneBot sender 字段中的 is_bot/role=bot/category 等)\n # 3) 启发式自介模板兜底(\"我是 X 助手\"、\"暂时还不能和你对话\" 等)\n catty_ignored_user_ids: set[int] = Field(default_factory=set)\n catty_ignore_marked_bots: bool = True\n catty_ignore_bot_self_intro_enabled: bool = True\n catty_http_proxy: str = \"\"\n catty_hot_reload_enabled: bool = True\n catty_hot_reload_poll_seconds: float = 1.5\n catty_hot_reload_debounce_seconds: float = 1.0\n catty_hot_reload_restart_on_code_change: bool = True\n\n @field_validator(\n \"catty_openai_base_url\",\n \"catty_audit_ai_base_url\",\n \"catty_vision_base_url\",\n \"catty_filter_base_url\",\n \"catty_local_critic_base_url\",\n )\n @classmethod\n def normalize_base_url(cls, value: str) -> str:\n return value.strip().rstrip(\"/\")\n\n @field_validator(\"catty_group_history_scope\")\n @classmethod\n def validate_group_history_scope(cls, value: str) -> str:\n normalized = value.strip().lower()\n if normalized not in {\"group\", \"user\"}:\n raise ValueError(\"catty_group_history_scope must be group or user\")\n return normalized\n\n @field_validator(\"catty_local_critic_mode\")\n @classmethod\n def validate_local_critic_mode(cls, value: str) -> str:\n normalized = value.strip().lower()\n aliases = {\n \"gate\": \"reply_gate_only\",\n \"gate_only\": \"reply_gate_only\",\n \"reply_gate\": \"reply_gate_only\",\n \"reply_gate_only\": \"reply_gate_only\",\n \"critic\": \"reply_gate_and_critic\",\n \"full\": \"reply_gate_and_critic\",\n \"reply_gate_and_critic\": \"reply_gate_and_critic\",\n }\n if normalized not in aliases:\n raise ValueError(\"catty_local_critic_mode must be reply_gate_only or reply_gate_and_critic\")\n return aliases[normalized]\n\n @field_validator(\n \"catty_trigger_prefixes\", \"catty_directed_keywords\", \"catty_web_search_engines\",\n \"catty_prompt_order\", \"catty_prompts_disabled\",\n mode=\"before\",\n )\n @classmethod\n def parse_prefixes(cls, value: Any) -> Any:\n if value is None:\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"catty_trigger_prefixes JSON value must be a list\")\n return [str(item).strip() for item in loaded if str(item).strip()]\n return _split_text(raw)\n return value\n\n @field_validator(\"catty_keyword_replies\", mode=\"before\")\n @classmethod\n def parse_keyword_replies(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return []\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return []\n return json.loads(raw)\n if isinstance(value, dict):\n return [{\"keywords\": [key], \"reply\": reply} for key, reply in value.items()]\n return value\n\n @field_validator(\n \"catty_allowed_user_ids\",\n \"catty_allowed_group_ids\",\n \"catty_ignored_user_ids\",\n \"catty_memory_special_group_ids\",\n \"catty_special_care_user_ids\",\n \"catty_game_context_star_resonance_group_ids\",\n \"catty_game_context_strinova_group_ids\",\n mode=\"before\",\n )\n @classmethod\n def parse_int_set(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return set()\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return set()\n if raw.startswith(\"[\"):\n loaded = json.loads(raw)\n if not isinstance(loaded, list):\n raise ValueError(\"value must be a JSON list\")\n return {int(item) for item in loaded}\n return {int(item) for item in _split_text(raw)}\n if isinstance(value, (list, tuple, set)):\n return {int(item) for item in value}\n return value\n\n @field_validator(\n \"catty_openai_extra_headers\",\n \"catty_ai_fallback_extra_headers\",\n \"catty_audit_ai_extra_headers\",\n \"catty_vision_extra_headers\",\n \"catty_filter_extra_headers\",\n \"catty_local_critic_extra_headers\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_headers(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n data = _parse_json_object(value)\n else:\n data = value\n return {str(key): str(val) for key, val in dict(data).items()}\n\n @field_validator(\n \"catty_openai_extra_body\",\n \"catty_ai_fallback_extra_body\",\n \"catty_audit_ai_extra_body\",\n \"catty_vision_extra_body\",\n \"catty_filter_extra_body\",\n \"catty_local_critic_extra_body\",\n mode=\"before\",\n )\n @classmethod\n def parse_extra_body(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return _parse_json_object(value)\n return value\n\n @field_validator(\"catty_group_titles\", \"catty_user_titles\", mode=\"before\")\n @classmethod\n def parse_title_map(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n if isinstance(value, str):\n return {str(key): str(val) for key, val in _parse_json_object(value).items()}\n return {str(key): str(val) for key, val in dict(value).items()}\n\n @field_validator(\"catty_group_user_titles\", mode=\"before\")\n @classmethod\n def parse_group_user_titles(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, dict[str, str]] = {}\n for group_id, members in data.items():\n parsed[str(group_id)] = {str(user_id): str(title) for user_id, title in dict(members).items()}\n return parsed\n\n @field_validator(\"catty_group_special_care_user_ids\", mode=\"before\")\n @classmethod\n def parse_group_special_care_user_ids(cls, value: Any) -> Any:\n if value is None or value == \"\":\n return {}\n data = _parse_json_object(value) if isinstance(value, str) else dict(value)\n parsed: dict[str, set[int]] = {}\n for group_id, members in data.items():\n if members is None or members == \"\":\n parsed[str(group_id)] = set()\n elif isinstance(members, str):\n parsed[str(group_id)] = {int(item) for item in _split_text(members)}\n elif isinstance(members, (list, tuple, set)):\n parsed[str(group_id)] = {int(item) for item in members}\n else:\n raise ValueError(\"catty_group_special_care_user_ids values must be lists or comma-separated strings\")\n return parsed\n\n @field_validator(\n \"catty_temperature\",\n \"catty_max_tokens\",\n \"catty_audit_ai_temperature\",\n \"catty_audit_ai_max_tokens\",\n \"catty_audit_ai_request_timeout\",\n \"catty_vision_temperature\",\n \"catty_vision_max_tokens\",\n \"catty_vision_request_timeout\",\n \"catty_vision_inline_max_wait_seconds\",\n \"catty_reply_queue_max_wait_seconds\",\n \"catty_filter_temperature\",\n \"catty_filter_max_tokens\",\n \"catty_filter_request_timeout\",\n \"catty_filter_group_batch_messages\",\n \"catty_filter_group_batch_seconds\",\n \"catty_filter_anger_warn_threshold\",\n \"catty_filter_anger_mute_threshold\",\n \"catty_filter_anger_cooldown_seconds\",\n \"catty_local_critic_temperature\",\n \"catty_local_critic_max_tokens\",\n \"catty_local_critic_request_timeout\",\n \"catty_local_critic_rewrite_when_score_below\",\n \"catty_local_critic_reply_gate_min_confidence\",\n \"catty_local_critic_reply_gate_examples\",\n \"catty_local_critic_reply_gate_max_tokens\",\n \"catty_local_critic_reply_gate_request_timeout\",\n \"catty_local_critic_reply_gate_user_message_chars\",\n \"catty_local_critic_reply_gate_plain_text_chars\",\n \"catty_local_critic_reply_gate_context_chars\",\n \"catty_local_critic_warmup_interval_seconds\",\n \"catty_local_critic_warmup_request_timeout\",\n \"catty_web_search_cooldown_seconds\",\n \"catty_web_search_max_results\",\n \"catty_web_search_request_timeout\",\n \"catty_nsfw_search_max_results\",\n \"catty_nsfw_search_request_timeout\",\n \"catty_nsfw_search_cooldown_seconds\",\n \"catty_nsfw_image_send_count\",\n \"catty_turtle_soup_cooldown_seconds\",\n \"catty_special_care_cooldown_seconds\",\n \"catty_special_care_response_window_minutes\",\n \"catty_soft_directed_reply_probability\",\n \"catty_direct_address_reply_probability\",\n \"catty_memory_reply_boost_min_corpus_messages\",\n \"catty_memory_reply_boost_probability_bonus\",\n \"catty_memory_reply_boost_max_probability\",\n \"catty_emoji_interest_threshold\",\n \"catty_emoji_save_interest_threshold\",\n \"catty_emoji_max_candidates\",\n \"catty_emoji_reply_probability\",\n \"catty_emoji_diversity_recent_window\",\n \"catty_emoji_diversity_candidate_pool\",\n \"catty_proactive_max_daily_per_group\",\n \"catty_proactive_check_interval_seconds\",\n \"catty_proactive_min_interval_minutes\",\n \"catty_proactive_response_window_minutes\",\n \"catty_proactive_recent_messages\",\n \"catty_proactive_active_window_minutes\",\n \"catty_proactive_active_min_messages\",\n \"catty_reply_human_split_max_chunks\",\n \"catty_hot_reload_poll_seconds\",\n \"catty_hot_reload_debounce_seconds\",\n mode=\"before\",\n )\n @classmethod\n def parse_optional_numbers(cls, value: Any) -> Any:\n if value == \"\":\n return None\n return value\n"}}}} |