Files

1 line
1.3 MiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}