Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chilishark27_ga-manager.json
T

1 line
197 KiB
JSON
Raw 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": "chilishark27/ga-manager", "files": {"ga_core/assets/configure_mykey.py": {"eaa7b6c1c04eecdcb522f2dd2333ff37c2ccaaa3": {"t": 1785595183.3033319, "n": 54456, "h": "121de44c6cb7c84c", "c": "#!/usr/bin/env python3\n\"\"\"\nGenericAgent — 交互式初始化向导 (configure.py)\n一键配置 LLM 模型 + 消息平台,自动生成 mykey.py\n\n用法:\n python configure.py\n\"\"\"\n\nimport ast\nimport os\nimport sys\nimport re\nimport shutil\nimport json\nimport urllib.request\nfrom datetime import datetime\n\n# ── ANSI 颜色 ──────────────────────────────────────────────────────────────\nC = {\n 'reset': '\\033[0m', 'bold': '\\033[1m', 'dim': '\\033[2m',\n 'red': '\\033[91m', 'green': '\\033[92m', 'yellow': '\\033[93m',\n 'blue': '\\033[94m', 'magenta': '\\033[95m', 'cyan': '\\033[96m', 'white': '\\033[97m',\n}\n\nPROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\nMYKPY_PATH = os.path.join(PROJECT_ROOT, 'mykey.py')\n\n# ── 模型厂商定义 ───────────────────────────────────────────────────────────\n\nLLM_PROVIDERS = [\n # ═══════════════════════════ 通用协议(官方直连或任意兼容中转)═══════════════════════════\n {\n 'id': 'oai_chat',\n 'name': 'OpenAI Chat Completions 协议',\n 'desc': '官方直连或任意 OAI 兼容中转/网关,自填 apibase(回车=OpenAI 官方)',\n 'type': 'native_oai',\n 'template': {\n 'name': 'gpt-native', 'apikey': 'sk-<your-key>',\n 'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.5',\n 'api_mode': 'chat_completions', 'reasoning_effort': 'high',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '官方在 https://platform.openai.com/api-keys 获取;中转站填其提供的 Key',\n 'model_choices': ['gpt-5.5', 'gpt-5.4'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.openai.com/v1'},\n ],\n },\n {\n 'id': 'oai_responses',\n 'name': 'OpenAI Responses 协议',\n 'desc': 'Responses API(o 系列/GPT-5.5 推荐端点),官方或兼容网关,自填 apibase',\n 'type': 'native_oai',\n 'template': {\n 'name': 'gpt-responses', 'apikey': 'sk-<your-key>',\n 'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.5',\n 'api_mode': 'responses', 'reasoning_effort': 'high',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '官方在 https://platform.openai.com/api-keys 获取;中转站填其提供的 Key',\n 'model_choices': ['gpt-5.5', 'gpt-5.4'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.openai.com/v1'},\n ],\n },\n {\n 'id': 'claude_messages',\n 'name': 'Claude Messages 协议',\n 'desc': 'Anthropic 官方直连或任意 Claude 兼容中转,自填 apibase(回车=官方)',\n 'type': 'native_claude',\n 'template': {\n 'name': 'anthropic-direct', 'apikey': 'sk-ant-<your-key>',\n 'apibase': 'https://api.anthropic.com', 'model': 'claude-opus-4-7',\n 'thinking_type': 'adaptive', 'max_tokens': 32768, 'temperature': 1,\n },\n 'key_hint': '官方在 https://console.anthropic.com/ 获取;中转站填其提供的 Key',\n 'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.anthropic.com'},\n ],\n },\n # ═══════════════════════════ 直连 API(按旗舰能力降序)═══════════════════════════\n {\n 'id': 'deepseek',\n 'name': 'DeepSeek (v4-Pro / Flash)',\n 'desc': '开源模型,v4-Pro 旗舰 1M 上下文',\n 'type': 'native_oai',\n 'template': {\n 'name': 'deepseek', 'apikey': 'sk-<your-deepseek-key>',\n 'apibase': 'https://api.deepseek.com', 'model': 'deepseek-v4-flash',\n 'api_mode': 'chat_completions', 'reasoning_effort': 'high',\n },\n 'key_hint': '在 https://platform.deepseek.com/api_keys 获取',\n 'model_choices': ['deepseek-v4-pro', 'deepseek-v4-flash'],\n },\n {\n 'id': 'kimi',\n 'name': 'Kimi (k2.6 / k2.5) 双协议',\n 'desc': '月之暗面,支持 Anthropic 和 OAI 双协议',\n 'type': 'native_claude',\n 'template': {\n 'name': 'kimi', 'apikey': 'sk-kimi-<your-key>',\n 'apibase': 'https://api.kimi.com/coding',\n 'model': 'kimi-for-coding', 'fake_cc_system_prompt': True,\n 'thinking_type': 'adaptive',\n },\n 'key_hint': '在 https://kimi.com/code 或 https://platform.moonshot.cn/ 获取',\n 'model_choices': ['kimi-k2.6', 'kimi-k2.5'],\n 'extra_fields': [\n {\n 'key': '_protocol', 'label': '选择 API 协议',\n 'type': 'choice',\n 'options': [\n {'id': 'native_claude', 'name': 'Anthropic 兼容 (推荐)', 'desc': 'kimi-for-coding 端点,CC 兼容', 'apibase': 'https://api.kimi.com/coding', 'fake_cc_system_prompt': True, 'model': 'kimi-for-coding'},\n {'id': 'native_oai', 'name': 'OpenAI 协议', 'desc': 'Moonshot OAI 端点,kimi-k2 系列', 'apibase': 'https://api.moonshot.cn/v1', 'model': 'kimi-k2.6'},\n ],\n },\n ],\n },\n {\n 'id': 'qwen',\n 'name': '阿里通义千问 (Qwen3.5 / 百炼)',\n 'desc': '阿里云百炼,Qwen3 系列百万级上下文',\n 'type': 'native_oai',\n 'template': {\n 'name': 'qwen', 'apikey': 'sk-<your-dashscope-key>',\n 'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n 'model': 'qwen3.5-plus',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://bailian.console.aliyun.com/ 获取 API Key',\n 'model_choices': ['qwen3.5-plus', 'qwen3-coder-plus'],\n 'extra_fields': [\n {\n 'key': '_endpoint', 'label': '选择端点',\n 'type': 'choice',\n 'options': [\n {'id': 'standard', 'name': '标准按量付费', 'desc': 'dashscope.aliyuncs.com,兼容模式', 'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1'},\n {'id': 'coding_plan', 'name': '百炼 Coding Plan (订阅)', 'desc': 'coding-intl.dashscope.aliyuncs.com,100万上下文', 'apibase': 'https://coding-intl.dashscope.aliyuncs.com/v1', 'context_win': 1000000},\n ],\n },\n ],\n },\n {\n 'id': 'zhipu',\n 'name': '智谱 GLM-5.1 (Coding Plan)',\n 'desc': '智谱 GLM,支持 Coding Plan CN (Anthropic) 和 Global (OAI) 双端点',\n 'type': 'native_claude',\n 'template': {\n 'name': 'zhipu-glm', 'apikey': 'sk-<your-zhipu-key>',\n 'apibase': 'https://open.bigmodel.cn/api/anthropic',\n 'model': 'GLM-5.1-Cloud', 'fake_cc_system_prompt': False,\n 'thinking_type': 'adaptive', 'max_retries': 3,\n 'connect_timeout': 10, 'read_timeout': 180,\n },\n 'key_hint': 'CN 在 https://open.bigmodel.cn/ 获取;Global 在 https://z.ai/ 获取',\n 'model_choices': ['GLM-5.1-Cloud', 'glm-4.7'],\n 'extra_fields': [\n {\n 'key': '_plan', 'label': '选择 Coding Plan',\n 'type': 'choice',\n 'options': [\n {'id': 'native_claude', 'name': 'Coding Plan CN (Anthropic)', 'desc': 'open.bigmodel.cn,推荐国内用户', 'apibase': 'https://open.bigmodel.cn/api/anthropic', 'fake_cc_system_prompt': False},\n {'id': 'native_oai', 'name': 'Coding Plan Global (OAI)', 'desc': 'api.z.ai,OpenAI 协议,全球可用', 'apibase': 'https://api.z.ai/api/paas/v4'},\n ],\n },\n ],\n },\n {\n 'id': 'minimax',\n 'name': 'MiniMax M3 (双协议)',\n 'desc': 'MiniMax M3,支持 Anthropic 和 OpenAI 双协议',\n 'type': 'native_claude',\n 'template': {\n 'name': 'minimax', 'apikey': 'eyJh...<your-minimax-key>',\n 'apibase': 'https://api.minimaxi.com/anthropic',\n 'model': 'MiniMax-M3', 'max_retries': 3,\n },\n 'key_hint': '在 https://platform.minimaxi.com/user-center/basic-information 获取',\n 'model_choices': ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],\n 'extra_fields': [\n {\n 'key': '_protocol', 'label': '选择 API 协议',\n 'type': 'choice',\n 'options': [\n {'id': 'native_claude', 'name': 'Anthropic 协议 (推荐)', 'desc': '无 <think> 标签,原生 Claude 兼容', 'apibase': 'https://api.minimaxi.com/anthropic'},\n {'id': 'native_oai', 'name': 'OpenAI 协议', 'desc': '走 /v1/chat/completions', 'apibase': 'https://api.minimaxi.com/v1', 'context_win': 50000},\n ],\n },\n ],\n },\n {\n 'id': 'stepfun',\n 'name': '阶跃星辰 Step-3.5 (推理强)',\n 'desc': '阶跃星辰 Step 系列,支持标准和 Step Plan 双端点',\n 'type': 'native_oai',\n 'template': {\n 'name': 'stepfun', 'apikey': 'sk-<your-stepfun-key>',\n 'apibase': 'https://api.stepfun.com/v1',\n 'model': 'step-3.5-flash',\n 'api_mode': 'chat_completions',\n 'context_win': 262144,\n },\n 'key_hint': '在 https://platform.stepfun.com/ 获取 API Key',\n 'model_choices': ['step-3.5-flash', 'step-3.5-flash-2603'],\n 'extra_fields': [\n {\n 'key': '_endpoint', 'label': '选择端点',\n 'type': 'choice',\n 'options': [\n {'id': 'standard', 'name': '标准端点', 'desc': 'api.stepfun.com/v1,按量付费', 'apibase': 'https://api.stepfun.com/v1', 'context_win': 262144},\n {'id': 'step_plan', 'name': 'Step Plan (订阅)', 'desc': 'api.stepfun.com/step_plan/v1,订阅制', 'apibase': 'https://api.stepfun.com/step_plan/v1', 'context_win': 262144},\n ],\n },\n ],\n },\n {\n 'id': 'qianfan',\n 'name': '百度千帆 (ERNIE 5.0 / 第三方)',\n 'desc': '百度智能云千帆,文心一言 ERNIE 5.0 + DeepSeek 等',\n 'type': 'native_oai',\n 'template': {\n 'name': 'baidu-qianfan', 'apikey': '<your-qianfan-key>',\n 'apibase': 'https://qianfan.baidubce.com/v2',\n 'model': 'ernie-5.0-thinking-preview',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://console.bce.baidu.com/qianfan/ 创建应用获取 API Key',\n 'model_choices': ['ernie-5.0-thinking-preview', 'deepseek-v3.2'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://qianfan.baidubce.com/v2'},\n ],\n },\n {\n 'id': 'volcengine',\n 'name': '火山引擎 (豆包 / Ark)',\n 'desc': '字节跳动火山引擎,支持标准 Ark 和 Ark Coding Plan',\n 'type': 'native_oai',\n 'template': {\n 'name': 'volc-ark', 'apikey': '<your-ark-api-key>',\n 'apibase': 'https://ark.cn-beijing.volces.com/api/v3',\n 'model': 'doubao-seed-code-preview-251028',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://console.volcengine.com/ark/ 创建推理接入点后获取 API Key',\n 'model_choices': ['doubao-seed-code-preview-251028', 'doubao-seed-1-8-251228'],\n 'extra_fields': [\n {\n 'key': '_endpoint', 'label': '选择端点',\n 'type': 'choice',\n 'options': [\n {'id': 'standard', 'name': '标准 Ark', 'desc': 'ark.cn-beijing.volces.com/api/v3,按量付费', 'apibase': 'https://ark.cn-beijing.volces.com/api/v3'},\n {'id': 'coding_plan', 'name': 'Ark Coding Plan (订阅)', 'desc': 'ark.cn-beijing.volces.com/api/coding/v3', 'apibase': 'https://ark.cn-beijing.volces.com/api/coding/v3'},\n ],\n },\n ],\n },\n {\n 'id': 'xiaomi',\n 'name': '小米 MiMo (MiMo 2.5 Pro / TokenPlan)',\n 'desc': '小米 MiMo 系列,超大上下文窗口,支持 TokenPlan 预付费',\n 'type': 'native_oai',\n 'template': {\n 'name': 'xiaomi-mimo', 'apikey': 'sk-<your-xiaomi-key>',\n 'apibase': 'https://api.xiaomimimo.com/v1',\n 'model': 'mimo-v2.5-pro',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://x.xiaomi.com/ 获取 API Key',\n 'model_choices': ['mimo-v2.5-pro', 'mimo-v2-flash'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://api.xiaomimimo.com/v1'},\n ],\n },\n {\n 'id': 'tencent_tokenhub',\n 'name': '腾讯混元 TokenHub (Hy3 / TokenPlan)',\n 'desc': '腾讯云 TokenHub,混元 Hy3 系列,TokenPlan 预付费',\n 'type': 'native_oai',\n 'template': {\n 'name': 'tencent-tokenhub', 'apikey': 'sk-<your-tokenhub-key>',\n 'apibase': 'https://tokenhub.tencentmaas.com/v1',\n 'model': 'hy3-preview',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://console.cloud.tencent.com/tokenhub 获取 API Key',\n 'model_choices': ['hy3-preview'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://tokenhub.tencentmaas.com/v1'},\n ],\n },\n # ═══════════════════════════ 代理 / 中继(支持 Claude/GPT 等顶级模型)══════════\n {\n 'id': 'cc_relay',\n 'name': 'CC Switch 透传 (社区常用)',\n 'desc': '社区 Claude Code 透传渠道,可接入 Claude Opus',\n 'type': 'native_claude',\n 'template': {\n 'name': 'cc-relay', 'apikey': 'sk-user-<your-relay-key>',\n 'apibase': 'https://<your-cc-switch-host>/claude/office',\n 'model': 'claude-opus-4-7', 'fake_cc_system_prompt': True,\n 'thinking_type': 'adaptive',\n },\n 'key_hint': '从你的 CC Switch 服务商获取 apikey 和 apibase',\n 'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://your-host/claude/office'},\n {'key': 'fake_cc_system_prompt', 'label': 'fake_cc_system_prompt', 'type': 'bool', 'default': True},\n ],\n },\n {\n 'id': 'openrouter',\n 'name': 'OpenRouter (多模型中继)',\n 'desc': '一个 Key 通吃 Claude/GPT/DeepSeek/Qwen 等',\n 'type': 'native_oai',\n 'template': {\n 'name': 'openrouter', 'apikey': 'sk-or-<your-openrouter-key>',\n 'apibase': 'https://openrouter.ai/api/v1',\n 'model': 'anthropic/claude-opus-4-7',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '在 https://openrouter.ai/keys 获取',\n 'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.5'],\n },\n {\n 'id': 'commonstack',\n 'name': 'CommonStack (统一网关)',\n 'desc': '一个 Key 通吃 Claude/GPT/Gemini/DeepSeek/MiniMax/Zhipu/xAI 等',\n 'type': 'native_oai',\n 'template': {\n 'name': 'commonstack', 'apikey': 'sk-<your-commonstack-key>',\n 'apibase': 'https://api.commonstack.ai/v1',\n 'model': 'anthropic/claude-opus-4-7',\n 'api_mode': 'chat_completions',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '在 https://commonstack.ai 注册后从 Dashboard 获取 API Key',\n 'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.5'],\n },\n {\n 'id': 'crs',\n 'name': 'CRS 反代 (Claude Max 多通道)',\n 'desc': 'CRS 协议的反代服务,支持 Claude Max / Gemini Ultra 通道',\n 'type': 'native_claude',\n 'template': {\n 'name': 'crs', 'apikey': 'cr_<your-crs-key>',\n 'apibase': 'https://<your-crs-host>/api',\n 'model': 'claude-opus-4-7[1m]', 'fake_cc_system_prompt': True,\n 'thinking_type': 'adaptive', 'max_tokens': 32768,\n 'max_retries': 3, 'read_timeout': 180,\n },\n 'key_hint': '从你的 CRS 服务商获取 key 和 host',\n 'model_choices': ['claude-opus-4-7[1m]', 'claude-sonnet-4-6'],\n 'extra_fields': [\n {\n 'key': '_channel', 'label': '选择 CRS 通道',\n 'type': 'choice',\n 'options': [\n {'id': 'claude_max', 'name': 'Claude Max (默认)', 'desc': '标准 CRS Claude 通道', 'apibase': 'https://<your-crs-host>/api'},\n {'id': 'gemini_ultra', 'name': 'Gemini Ultra (Antigravity)', 'desc': 'CRS 包装的 Google Antigravity,不支持 SSE 流式', 'apibase': 'https://<your-crs-gemini-host>/antigravity/api', 'model': 'claude-opus-4-7-thinking', 'stream': False},\n ],\n },\n ],\n },\n {\n 'id': 'gmi',\n 'name': 'GMI Serving (通用模型中继)',\n 'desc': 'GMI 通用模型推理服务,支持多种开源/闭源(手动输入模型名)',\n 'type': 'native_oai',\n 'template': {\n 'name': 'gmi', 'apikey': '<your-gmi-key>',\n 'apibase': 'https://api.gmi-serving.com/v1',\n 'model': 'gmi-default',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '从 GMI 服务商获取 API Key,探测失败时手动输入模型名',\n 'model_choices': [], # 中继服务,模型由服务商提供,探测失败时手动输入\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://api.gmi-serving.com/v1'},\n ],\n },\n]\n\n# ── 消息平台定义 ────────────────────────────────────────────────────────────\nPLATFORMS = [\n {\n 'id': 'none',\n 'name': '不使用消息平台(纯终端 REPL)',\n 'desc': '直接用 python agentmain.py 在终端交互',\n 'deps': [],\n },\n {\n 'id': 'telegram',\n 'name': 'Telegram 机器人',\n 'desc': '通过 Telegram Bot 与 Agent 对话',\n 'file': 'frontends/tgapp.py',\n 'deps': ['python-telegram-bot'],\n 'env_vars': [\n {'key': 'tg_bot_token', 'label': 'Bot Token', 'hint': '从 @BotFather 获取'},\n {'key': 'tg_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'qq',\n 'name': 'QQ 机器人',\n 'desc': '通过 QQ 官方机器人 API 接入',\n 'file': 'frontends/qqapp.py',\n 'deps': ['qq-botpy'],\n 'env_vars': [\n {'key': 'qq_app_id', 'label': 'App ID', 'hint': 'QQ 开放平台获取'},\n {'key': 'qq_app_secret', 'label': 'App Secret'},\n {'key': 'qq_allowed_users', 'label': '允许的用户 OpenID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'feishu',\n 'name': '飞书机器人',\n 'desc': '通过飞书应用与 Agent 对话',\n 'file': 'frontends/fsapp.py',\n 'deps': ['lark-oapi'],\n 'env_vars': [\n {'key': 'fs_app_id', 'label': 'App ID', 'hint': '飞书开放平台获取'},\n {'key': 'fs_app_secret', 'label': 'App Secret'},\n {'key': 'fs_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'wecom',\n 'name': '企业微信机器人',\n 'desc': '通过企业微信 Bot 接入',\n 'file': 'frontends/wecomapp.py',\n 'deps': ['wecombot'],\n 'env_vars': [\n {'key': 'wecom_bot_id', 'label': 'Bot ID'},\n {'key': 'wecom_secret', 'label': 'Bot Secret'},\n {'key': 'wecom_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'dingtalk',\n 'name': '钉钉机器人',\n 'desc': '通过钉钉应用接入',\n 'file': 'frontends/dingtalkapp.py',\n 'deps': ['dingtalk-sdk'],\n 'env_vars': [\n {'key': 'dingtalk_client_id', 'label': 'Client ID (App Key)'},\n {'key': 'dingtalk_client_secret', 'label': 'Client Secret (App Secret)'},\n {'key': 'dingtalk_allowed_users', 'label': '允许的用户 StaffID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'discord',\n 'name': 'Discord 机器人',\n 'desc': '通过 Discord Bot 接入',\n 'file': 'frontends/dcapp.py',\n 'deps': ['discord.py'],\n 'env_vars': [\n {'key': 'dc_bot_token', 'label': 'Bot Token', 'hint': 'Discord Developer Portal 获取'},\n {'key': 'dc_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'wechat',\n 'name': '微信 (iLink 协议)',\n 'desc': '通过微信个人号与 Agent 对话,扫码自动登录',\n 'file': 'frontends/wechatapp.py',\n 'deps': ['requests', 'qrcode', 'pycryptodome'],\n 'env_vars': [],\n },\n]\n\n\ndef _masked(v, reveal, tail):\n \"\"\"生成脱敏字符串:前 reveal 位明文 + * + 后 tail 位明文\"\"\"\n if len(v) > reveal + tail:\n return v[:reveal] + '*' * min(len(v) - reveal - tail, 8) + v[-tail:]\n elif len(v) > reveal:\n return v[:reveal] + '*' * (len(v) - reveal)\n return v\n\ndef masked_input(prompt, reveal=6, tail=4):\n \"\"\"密文输入,支持粘贴:批读取 + 延迟重绘,避免快速键入时丢字符。\n\n prompt 必须为单行(不含 \\\\n)。\n \"\"\"\n sys.stdout.write(prompt)\n sys.stdout.flush()\n chars = []\n\n def _repaint():\n m = _masked(''.join(chars), reveal, tail)\n sys.stdout.write(f'\\r{prompt}{m} \\r{prompt}{m}')\n sys.stdout.flush()\n\n def _process(c):\n \"\"\"处理单个字符,返回 True 表示应退出。\"\"\"\n if c in ('\\r', '\\n'):\n return True\n if c in ('\\x03', '\\x04'):\n raise KeyboardInterrupt\n if c in ('\\x08', '\\x7f'):\n if chars:\n chars.pop()\n elif c.isprintable() or c == ' ':\n chars.append(c)\n return False\n\n if os.name == 'nt':\n import msvcrt\n while True:\n c = msvcrt.getwch()\n if _process(c):\n break\n if c in ('\\x08', '\\x7f'):\n _repaint() # 退格立即重绘\n continue\n if not (c.isprintable() or c == ' '):\n continue\n # 批量读取:粘贴时一次取完\n while msvcrt.kbhit():\n c2 = msvcrt.getwch()\n if _process(c2):\n value = ''.join(chars)\n _repaint()\n sys.stdout.write('\\n')\n sys.stdout.flush()\n return value\n _repaint()\n else:\n import tty, termios, select\n fd = sys.stdin.fileno()\n old = termios.tcgetattr(fd)\n try:\n tty.setraw(fd)\n while True:\n c = sys.stdin.read(1)\n if _process(c):\n break\n if c in ('\\x08', '\\x7f'):\n _repaint() # 退格立即重绘\n continue\n if not (c.isprintable() or c == ' '):\n continue\n # 批量读取:只要 stdin 有数据就继续读,不重绘\n while select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):\n c2 = sys.stdin.read(1)\n if _process(c2):\n value = ''.join(chars)\n _repaint()\n termios.tcsetattr(fd, termios.TCSADRAIN, old)\n sys.stdout.write('\\n')\n sys.stdout.flush()\n return value\n _repaint()\n finally:\n termios.tcsetattr(fd, termios.TCSADRAIN, old)\n\n value = ''.join(chars)\n _repaint()\n sys.stdout.write('\\n')\n sys.stdout.flush()\n return value\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# UI Helpers\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef cprint(text, color=None, bold=False, end='\\n'):\n parts = []\n if color: parts.append(C.get(color, ''))\n if bold: parts.append(C['bold'])\n parts.append(text)\n parts.append(C['reset'])\n print(''.join(parts), end=end)\n\ndef banner():\n print('\\033[2J\\033[H', end='') # ANSI 清屏,跨平台\n print(f\"{C['cyan']}{C['bold']}\")\n print(\" ╔═══════════════════════════════════════════════════════════╗\")\n print(\" ║ GenericAgent — 交互式初始化向导 v1.2 ║\")\n print(\" ║ 一键配置 LLM 模型 + 消息平台,自动生成 mykey.py ║\")\n print(\" ╚═══════════════════════════════════════════════════════════╝\")\n print(f\"{C['reset']}\")\n print(f\"{C['dim']} 项目目录: {PROJECT_ROOT}{C['reset']}\")\n print()\n\ndef _check_python():\n \"\"\"检查 Python 版本,返回 (ok, msg)\"\"\"\n vi = sys.version_info\n if vi < (3, 10):\n return False, f\"Python {vi.major}.{vi.minor} 不满足最低要求 (≥ 3.10)\"\n if vi[:2] == (3, 12):\n return True, ''\n return True, f\"⚠ 当前 Python {vi.major}.{vi.minor},推荐使用 Python 3.12\"\n\ndef ask_choice(prompt, choices, allow_multi=False, default=None):\n \"\"\"交互式选择,返回 selected_id 或 [selected_ids]\"\"\"\n print(f\"\\n{C['bold']}{prompt}{C['reset']}\")\n if allow_multi:\n print(f\"{C['dim']} (可多选,输入序号用逗号分隔,如: 1,3,5;输入 a 全选;回车跳过){C['reset']}\")\n else:\n print(f\"{C['dim']} (输入序号,如: 1){C['reset']}\")\n for i, c in enumerate(choices, 1):\n desc = c.get('desc', '')\n print(f\" {C['green']}{i}.{C['reset']} {C['bold']}{c['name']}{C['reset']} {C['dim']}{desc}{C['reset']}\")\n while True:\n raw = input(f\"\\n {C['yellow']}►{C['reset']} \").strip()\n if not raw and default is not None:\n return default\n if allow_multi:\n if raw.lower() == 'a':\n return [c['id'] for c in choices]\n parts = [p.strip() for p in raw.split(',') if p.strip()]\n selected = []\n for p in parts:\n try:\n idx = int(p) - 1\n if 0 <= idx < len(choices):\n selected.append(choices[idx]['id'])\n except ValueError:\n pass\n if selected:\n return selected\n else:\n try:\n idx = int(raw) - 1\n if 0 <= idx < len(choices):\n return choices[idx]['id']\n except ValueError:\n pass\n print(f\" {C['red']}✗ 请输入有效序号{C['reset']}\")\n\ndef ask_input(prompt, default=None, secret=False, hint=None):\n \"\"\"交互式输入。secret=True 时使用脱敏输入。\"\"\"\n if hint:\n cprint(f\" {hint}\", 'dim')\n if default is not None:\n cprint(f\" [默认: {default}]\", 'dim')\n prompt_line = f\" {C['yellow']}►{C['reset']} {prompt}: \"\n while True:\n if secret:\n val = masked_input(prompt_line).strip()\n else:\n val = input(prompt_line).strip()\n if not val and default is not None:\n return default\n if val:\n return val\n cprint(\"✗ 此项不能为空\", 'red')\n\ndef ask_yesno(prompt, default=True):\n hint = \"Y/N\"\n raw = input(f\"\\n {C['yellow']}►{C['reset']} {prompt} ({hint}): \").strip().lower()\n if not raw:\n return default\n return raw.startswith('y')\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# LLM 配置逻辑\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef _get_proxy_handler():\n \"\"\"从环境变量读取代理配置,返回 ProxyHandler 或 None\"\"\"\n for var in ('HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'):\n url = os.environ.get(var)\n if url:\n return urllib.request.ProxyHandler({'https': url, 'http': url})\n return None\n\ndef probe_models(provider, apikey, apibase=None):\n \"\"\"调用 API 探测可用模型列表,返回模型 ID 列表或 None\"\"\"\n ptype = provider.get('type', 'native_oai')\n base = (apibase or provider['template'].get('apibase', '')).rstrip('/')\n\n if ptype == 'native_claude':\n url = f\"{base}/v1/models\"\n headers = {'x-api-key': apikey, 'anthropic-version': '2023-06-01', 'User-Agent': 'GenericAgent/1.0'}\n else:\n url = f\"{base}/models\"\n headers = {'Authorization': f'Bearer {apikey}', 'User-Agent': 'GenericAgent/1.0'}\n\n print(f\"\\n {C['dim']}🔍 正在探测可用模型 ({base}/models)...{C['reset']}\", end='', flush=True)\n if ptype == 'native_claude':\n print(f\" {C['dim']}(Anthropic 协议,探测可能失败){C['reset']}\", end='', flush=True)\n\n opener = urllib.request.build_opener()\n ph = _get_proxy_handler()\n if ph:\n opener = urllib.request.build_opener(ph)\n print(f\" {C['dim']}(via proxy){C['reset']}\", end='', flush=True)\n\n for attempt in range(2):\n try:\n req = urllib.request.Request(url, headers=headers, method='GET')\n with opener.open(req, timeout=10) as resp:\n data = json.loads(resp.read().decode())\n models = data.get('data', [])\n ids = sorted(set(m['id'] for m in models if isinstance(m, dict) and m.get('id')))\n if ids:\n print(f\" {C['green']}✓ 发现 {len(ids)} 个模型{C['reset']}\")\n return ids\n print(f\" {C['yellow']}⚠ 返回为空{C['reset']}\")\n return None\n except Exception as e:\n if attempt == 0 and 'timeout' in type(e).__name__.lower():\n print(f\" {C['yellow']}⏱ 超时,重试...{C['reset']}\", end='', flush=True)\n continue\n print(f\" {C['yellow']}⚠ 探测失败: {type(e).__name__}(将使用预设列表){C['reset']}\")\n return None\n return None\n\ndef _normalize_model_choices(choices):\n \"\"\"统一 model_choices 格式为 [{'id': str, 'name': str}]\"\"\"\n if not choices:\n return []\n result = []\n for item in choices:\n if isinstance(item, str):\n result.append({'id': item, 'name': item})\n elif isinstance(item, dict):\n result.append(item)\n elif isinstance(item, (tuple, list)) and len(item) >= 1:\n result.append({'id': item[0], 'name': item[1] if len(item) > 1 else item[0]})\n return result\n\ndef _configure_advanced(provider, cfg):\n \"\"\"配置高级可选字段: proxy, context_win, stream, user_agent, thinking_budget_tokens\"\"\"\n print(f\"\\n {C['dim']}── 高级选项(回车跳过,使用默认值){C['reset']}\")\n proxy = ask_input(\"HTTP 代理地址 (proxy)\", default='', hint='如 http://127.0.0.1:2082,留空跳过')\n if proxy:\n cfg['proxy'] = proxy\n cw = ask_input(\"上下文窗口阈值 (context_win)\", default='', hint='NativeClaude 默认 28000,其他默认 24000')\n if cw:\n cfg['context_win'] = int(cw)\n if cfg.get('thinking_type') == 'enabled':\n tbt = ask_input(\"thinking_budget_tokens\", default='', hint='low≈4096, medium≈10240, high≈32768')\n if tbt:\n cfg['thinking_budget_tokens'] = int(tbt)\n if cfg.get('type', provider['type']) == 'native_claude':\n ua = ask_input(\"User-Agent 版本号\", default='', hint='某些中转按 UA 白名单校验,pin 老版本用')\n if ua:\n cfg['user_agent'] = ua\n stream_default = cfg.get('stream', True)\n if ask_yesno(\"启用 SSE 流式 (stream)\", default=stream_default):\n cfg['stream'] = True\n else:\n cfg['stream'] = False\n\ndef configure_llm(provider):\n \"\"\"引导用户配置单个模型\"\"\"\n print(f\"\\n{C['cyan']}{'─'*60}{C['reset']}\")\n print(f\"{C['bold']} 配置: {provider['name']}{C['reset']}\")\n print(f\" {C['dim']}{provider['desc']}{C['reset']}\")\n print(f\"{C['cyan']}{'─'*60}{C['reset']}\")\n\n cfg = dict(provider['template'])\n\n # API Key(密文输入)\n cfg['apikey'] = ask_input(\n f\"API Key\",\n hint=provider.get('key_hint', ''),\n secret=True,\n )\n\n # 额外字段\n for field in provider.get('extra_fields', []):\n if field['key'] == 'apibase':\n cfg['apibase'] = ask_input(\n field['label'],\n default=field.get('default', cfg.get('apibase', '')),\n )\n elif field.get('type') == 'bool':\n cfg[field['key']] = ask_yesno(\n field['label'],\n default=field.get('default', True)\n )\n elif field.get('type') == 'choice':\n picked = ask_choice(field['label'], field['options'])\n chosen = next(o for o in field['options'] if o['id'] == picked)\n for opt_key, opt_val in chosen.items():\n if opt_key not in ('id', 'name', 'desc'):\n cfg[opt_key] = opt_val\n\n # 模型选择\n manual_choice = {'id': '__manual__', 'name': '✏️ 手动输入模型名', 'desc': '自定义模型 ID,不依赖探测结果'}\n model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase'))\n if model_list:\n refresh_choice = {'id': '__refresh__', 'name': '🔄 重新探测'}\n choices = [refresh_choice, manual_choice] + [{'id': m, 'name': m} for m in model_list]\n while True:\n picked = ask_choice(\"API 探测到以下可用模型(或手动输入):\", choices)\n if picked == '__refresh__':\n print(f\" {C['dim']}再次探测...{C['reset']}\")\n model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase'))\n if not model_list:\n print(f\" {C['yellow']}⚠ 再次探测失败{C['reset']}\")\n picked = _fallback_model(provider, manual_choice)\n break\n choices = [refresh_choice, manual_choice] + [{'id': m, 'name': m} for m in model_list]\n elif picked == '__manual__':\n picked = ask_input(\"请输入模型名\", default=cfg.get('model', ''))\n break\n else:\n break\n cfg['model'] = picked\n else:\n cfg['model'] = _fallback_model(provider, manual_choice)\n\n # 别名\n default_name = cfg.get('name', provider['id'])\n name = ask_input(\"此配置的别名 (name,Mixin 引用用)\", default=default_name)\n if name:\n cfg['name'] = name\n\n # 高级选项\n if ask_yesno(\"配置高级选项(proxy / context_win / stream 等)?\", default=False):\n _configure_advanced(provider, cfg)\n\n return cfg\n\ndef _fallback_model(provider, manual_choice=None):\n \"\"\"使用预设模型列表让用户选择,始终提供手动输入选项\"\"\"\n manual_choice = manual_choice or {'id': '__manual__', 'name': '✏️ 手动输入模型名', 'desc': '自定义模型 ID'}\n normalized = _normalize_model_choices(provider.get('model_choices', []))\n if normalized:\n choices = [manual_choice] + normalized\n picked = ask_choice(\"选择模型(或手动输入):\", choices)\n if picked == '__manual__':\n return ask_input(\"请输入模型名\", default=provider['template'].get('model', ''))\n return picked\n return ask_input(\"请输入模型名\", default=provider['template'].get('model', ''))\n\ndef configure_llms():\n \"\"\"配置 LLM 模型\"\"\"\n print(f\"\\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗\")\n print(f\"║ 第一步: 配置 LLM 模型 ║\")\n print(f\"╚══════════════════════════════════════╝{C['reset']}\")\n print(f\"\\n{C['dim']} 你可以配置最多 2 个模型组成故障转移 (Mixin) 列表。{C['reset']}\")\n\n all_cfgs = []\n provider_id = ask_choice(\"选择模型厂商 (配置第 1 个模型):\", LLM_PROVIDERS)\n provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id)\n cfg = configure_llm(provider)\n all_cfgs.append(cfg)\n\n if ask_yesno(\"再添加一个模型做故障转移?\", default=False):\n providers_ext = [{'id': '__stop__', 'name': '✓ 不需要备选了', 'desc': ''}] + LLM_PROVIDERS\n provider_id = ask_choice(\n \"选择模型厂商 (配置第 2 个模型 — 或选「不需要备选了」跳过):\",\n providers_ext\n )\n if provider_id != '__stop__':\n provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id)\n cfg = configure_llm(provider)\n all_cfgs.append(cfg)\n\n return all_cfgs\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# 消息平台配置逻辑\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef configure_platforms():\n \"\"\"配置消息平台,返回 (platform_configs, pip_hints)\"\"\"\n print(f\"\\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗\")\n print(f\"║ 第二步: 配置消息平台 ║\")\n print(f\"╚══════════════════════════════════════╝{C['reset']}\")\n print(f\"\\n{C['dim']} 消息平台用于从聊天软件与 Agent 交互。{C['reset']}\")\n print(f\"{C['dim']} 你也可以跳过此步,直接用终端 REPL。{C['reset']}\")\n\n platform_ids = ask_choice(\n \"选择消息平台 (可多选,选 '不使用' 则跳过):\",\n PLATFORMS,\n allow_multi=True,\n default=['none']\n )\n\n if 'none' in platform_ids:\n return [], set()\n\n selected_platforms = []\n pip_hints = set()\n\n for pid in platform_ids:\n platform = next(p for p in PLATFORMS if p['id'] == pid)\n pip_hints.update(platform.get('deps', []))\n\n print(f\"\\n{C['cyan']}{'─'*60}{C['reset']}\")\n print(f\"{C['bold']} 配置: {platform['name']}{C['reset']}\")\n print(f\"{C['cyan']}{'─'*60}{C['reset']}\")\n\n env_vals = {}\n\n if pid == 'feishu' and ask_yesno(\"使用一键扫码创建应用?(推荐)\", default=True):\n env_vals = _feishu_scan(platform)\n if pid == 'wechat' and ask_yesno(\"扫码登录微信 iLink?(推荐)\", default=True):\n env_vals = _wechat_scan()\n\n for var in platform['env_vars']:\n if var['key'] not in env_vals:\n env_vals.update(_manual_platform_var(var))\n\n if pid == 'wecom' and ask_yesno(\"设置欢迎消息?\", default=False):\n env_vals['wecom_welcome_message'] = ask_input(\"欢迎消息内容\", default='你好,我在线上。')\n\n selected_platforms.append({'platform': platform, 'config': env_vals})\n\n return selected_platforms, pip_hints\n\ndef _manual_platform_var(var):\n \"\"\"手动填写单个平台变量\"\"\"\n val = ask_input(var['label'], hint=var.get('hint', ''), default=var.get('default'))\n if var.get('is_list'):\n if val == '[]' or not val:\n return {var['key']: []}\n return {var['key']: [x.strip() for x in val.split(',') if x.strip()]}\n return {var['key']: val}\n\ndef _feishu_scan(platform):\n \"\"\"飞书一键扫码创建应用,返回 env_vals 或空 dict\"\"\"\n from io import StringIO\n try:\n import lark_oapi as lark\n import qrcode, threading\n except ImportError:\n print(f\"\\n {C['yellow']}⚠ lark-oapi 未安装,降级为手动配置{C['reset']}\")\n return {}\n\n print(f\"\\n {C['cyan']}📱 正在启动一键创建...{C['reset']}\")\n print(f\" {C['dim']} 请用飞书 App 扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\\n\")\n\n qr_printed = threading.Event()\n result_holder = {'data': None}\n\n def handle_qr(info):\n url = info['url']\n expire = info['expire_in']\n qr = qrcode.QRCode(border=1, box_size=1)\n qr.add_data(url)\n buf = StringIO()\n qr.print_ascii(out=buf)\n qr_art = buf.getvalue()\n print(f\"\\n {C['bold']}请用飞书扫描下方二维码,或复制链接在浏览器打开:{C['reset']}\")\n print(f\" {C['green']}{qr_art.replace(chr(27), '')}{C['reset']}\")\n print(f\" {C['dim']} 链接: {url}{C['reset']}\")\n print(f\" {C['dim']} 有效期 {expire} 秒{C['reset']}\")\n qr_printed.set()\n\n def handle_status(info):\n status = info['status']\n if status == 'polling':\n print(f\" {C['yellow']}⏳ 等待扫码...{C['reset']}\")\n elif status == 'slow_down':\n print(f\" {C['yellow']}⏳ 等待中... (间隔 {info.get('interval', '?')}s){C['reset']}\")\n elif status == 'domain_switched':\n print(f\" {C['cyan']}🌐 已切换认证域名{C['reset']}\")\n\n def run_register():\n try:\n result = lark.register_app(\n on_qr_code=handle_qr,\n on_status_change=handle_status,\n )\n result_holder['data'] = result\n except Exception as e:\n print(f\"\\n {C['red']}✗ 创建失败: {e}{C['reset']}\")\n\n thread = threading.Thread(target=run_register, daemon=True)\n thread.start()\n qr_printed.wait(timeout=15)\n thread.join(timeout=300)\n\n if result_holder['data']:\n result = result_holder['data']\n print(f\"\\n {C['green']}✅ 应用创建成功!{C['reset']}\")\n print(f\" App ID: {C['bold']}{result['client_id']}{C['reset']}\")\n print(f\" App Secret: {C['bold']}{result['client_secret']}{C['reset']}\")\n return {\n 'fs_app_id': result['client_id'],\n 'fs_app_secret': result['client_secret'],\n }\n else:\n print(f\"\\n {C['yellow']}⚠ 扫码创建未完成,降级为手动填写...{C['reset']}\")\n return {}\n\n\ndef _wechat_scan():\n \"\"\"微信 iLink 扫码登录,保存 token 到 ~/.wxbot/token.json,返回 env_vals\"\"\"\n print(f\"\\n {C['cyan']}📱 正在启动微信 iLink 扫码登录...{C['reset']}\")\n print(f\" {C['dim']} 请用微信扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\\n\")\n\n # 确保项目根在路径中,以便导入 frontends/wechatapp\n if PROJECT_ROOT not in sys.path:\n sys.path.insert(0, PROJECT_ROOT)\n try:\n from frontends.wechatapp import WxBotClient\n except ImportError as e:\n print(f\"\\n {C['yellow']}⚠ 无法导入 WxBotClient: {e}{C['reset']}\")\n return {}\n\n try:\n bot = WxBotClient()\n if bot.token:\n print(f\" {C['green']}✅ 已有有效 token (bot_id={bot.bot_id}){C['reset']}\")\n if ask_yesno(\"重新扫码登录?\", default=False):\n bot.token = ''\n else:\n return {}\n bot.login_qr()\n print(f\"\\n {C['green']}✅ 微信 iLink 扫码登录成功!{C['reset']}\")\n print(f\" Bot ID: {C['bold']}{bot.bot_id}{C['reset']}\")\n print(f\" Token 已保存到: {C['dim']}{bot._tf}{C['reset']}\")\n except Exception as e:\n print(f\"\\n {C['red']}✗ 扫码登录失败: {e}{C['reset']}\")\n return {}\n\n return {}\n\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# 生成 mykey.py\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef _var_type_info(cfg):\n \"\"\"根据配置类型返回 (var_prefix, session_type)\"\"\"\n cfg_type = cfg.get('type', 'native_oai')\n if cfg_type == 'native_claude':\n return 'native_claude_config', 'NativeClaudeSession'\n elif cfg_type == 'claude':\n return 'claude_config', 'ClaudeSession'\n elif cfg_type == 'oai':\n return 'oai_config', 'LLMSession'\n else:\n return 'native_oai_config', 'NativeOAISession'\n\n\ndef generate_mykey(llm_cfgs, platform_configs):\n \"\"\"生成 mykey.py 内容\"\"\"\n lines = []\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(f\"# GenericAgent — mykey.py (由 configure.py 自动生成 @ {datetime.now().strftime('%Y-%m-%d %H:%M')})\")\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"\")\n lines.append(\"# ── 停止符 ──────────────────────────────────────────────────────────────────\")\n lines.append(\"_SETUP_DONE = 'configure.py' # 删除此行可重新触发配置向导\")\n lines.append(\"\")\n\n # Mixin 配置\n names = [c['name'] for c in llm_cfgs]\n lines.append(\"# ── Mixin 故障转移 ──────────────────────────────────────────────────────────\")\n lines.append(\"mixin_config = {\")\n lines.append(f\" 'llm_nos': {names},\")\n lines.append(\" 'max_retries': 10,\")\n lines.append(\" 'base_delay': 0.5,\")\n lines.append(\"}\")\n lines.append(\"\")\n\n # 各模型配置\n type_counts = {}\n for cfg in llm_cfgs:\n cfg_type = cfg.get('type', 'native_oai')\n type_counts[cfg_type] = type_counts.get(cfg_type, 0) + 1\n\n type_indices = {}\n for i, cfg in enumerate(llm_cfgs):\n cfg_type = cfg.get('type', 'native_oai')\n var_prefix, session_type = _var_type_info(cfg)\n idx = type_indices.get(cfg_type, 0)\n type_indices[cfg_type] = idx + 1\n\n if type_counts[cfg_type] > 1:\n var_name = f\"{var_prefix}_{idx}\"\n else:\n var_name = var_prefix\n\n lines.append(f\"# ── {cfg['name']} ({session_type}) ─────────────────────────────────────────────\")\n lines.append(f\"{var_name} = {{\")\n _write_config_fields(lines, cfg)\n lines.append(\"}\")\n lines.append(\"\")\n\n # 平台配置\n if platform_configs:\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"# 聊天平台集成\")\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"\")\n for pc in platform_configs:\n for key, val in pc['config'].items():\n _write_platform_value(lines, key, val)\n lines.append(\"\")\n\n # 尾部\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"# 配置完毕!运行: python agentmain.py (终端 REPL)\")\n if platform_configs:\n for pc in platform_configs:\n p = pc['platform']\n lines.append(f\"# 或: python {p['file']} ({p['name']})\")\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n\n return '\\n'.join(lines)\n\ndef _write_config_fields(lines, cfg):\n \"\"\"写入配置字典的键值对(缩进的 'key': value, 格式)\"\"\"\n for key in ['name', 'type', 'apikey', 'apibase', 'model', 'api_mode',\n 'fake_cc_system_prompt', 'thinking_type', 'thinking_budget_tokens',\n 'reasoning_effort', 'max_tokens', 'max_retries', 'connect_timeout',\n 'read_timeout', 'temperature', 'context_win',\n 'proxy', 'user_agent', 'stream']:\n if key not in cfg:\n continue\n val = cfg[key]\n if isinstance(val, bool):\n lines.append(f\" '{key}': {str(val)},\")\n elif isinstance(val, (int, float)):\n lines.append(f\" '{key}': {val},\")\n elif isinstance(val, str):\n lines.append(f\" '{key}': '{val}',\")\n else:\n lines.append(f\" '{key}': {repr(val)},\")\n\ndef _write_platform_value(lines, key, val):\n \"\"\"写入顶级变量(平台配置等)\"\"\"\n if isinstance(val, list):\n if val:\n lines.append(f\"{key} = {repr(val)}\")\n else:\n lines.append(f\"{key} = [] # 允许所有用户\")\n elif isinstance(val, str):\n lines.append(f\"{key} = '{val}'\")\n else:\n lines.append(f\"{key} = {repr(val)}\")\n\n\ndef _parse_existing_mykey():\n \"\"\"解析已有 mykey.py,返回 (model_names, platform_infos)\n\n model_names: [str] — 模型名列表\n platform_infos: [{'id': str, 'vars': [{'key': str, 'val': ...}]}] — 平台信息\n 解析失败时返回 ([], [])\n \"\"\"\n if not os.path.exists(MYKPY_PATH):\n return [], []\n\n with open(MYKPY_PATH, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # 解析模型名\n model_names = []\n m = re.search(r\"'llm_nos':\\s*\\[([^\\]]*)\\]\", content)\n if m:\n model_names = re.findall(r\"'([^']+)'\", m.group(1))\n\n # 先收集所有已知平台 env var key → 判断值类型\n all_env_var_keys = {}\n platform_env_keys = {} # pid -> [var_key]\n for p in PLATFORMS:\n pid = p['id']\n platform_env_keys.setdefault(pid, [])\n for var in p.get('env_vars', []):\n vkey = var['key']\n all_env_var_keys[vkey] = var\n platform_env_keys[pid].append(vkey)\n\n # 逐平台解析所有已知变量\n platform_infos = []\n for pid, env_keys in platform_env_keys.items():\n vars_found = []\n for vkey in env_keys:\n var_def = all_env_var_keys[vkey]\n val = None\n if var_def.get('is_list'):\n # 匹配 `xxx = [...]`\n m_var = re.search(rf\"^{vkey}\\s*=\\s*(\\[[^\\]]*\\])\", content, re.MULTILINE)\n if m_var:\n try:\n val = ast.literal_eval(m_var.group(1))\n except (ValueError, SyntaxError):\n pass\n else:\n # 匹配 `xxx = '...'`\n m_var = re.search(rf\"^{vkey}\\s*=\\s*'([^']*)'\", content, re.MULTILINE)\n if m_var:\n val = m_var.group(1)\n if val is not None:\n vars_found.append({'key': vkey, 'val': val})\n if vars_found:\n platform_infos.append({'id': pid, 'vars': vars_found})\n\n return model_names, platform_infos\n\n\ndef _parse_existing_llm_cfgs():\n \"\"\"解析已有 mykey.py,返回完整 LLM 配置字典列表 [{name, apikey, ...}]\n 解析失败时返回 []\n \"\"\"\n if not os.path.exists(MYKPY_PATH):\n return []\n\n with open(MYKPY_PATH, 'r', encoding='utf-8') as f:\n content = f.read()\n\n cfgs = []\n # 匹配所有 `xxx = { ... }` 顶层字典赋值\n # 用简单状态机: 找 `\\w+ = {` 然后匹配花括号\n pattern = re.compile(r'^(\\w+)\\s*=\\s*\\{', re.MULTILINE)\n for m in pattern.finditer(content):\n brace_start = m.end() - 1 # '{' 的位置\n depth = 1\n i = brace_start + 1\n while i < len(content) and depth > 0:\n if content[i] == '{':\n depth += 1\n elif content[i] == '}':\n depth -= 1\n i += 1\n if depth == 0:\n dict_text = content[m.end():i - 1]\n try:\n d = ast.literal_eval('{' + dict_text + '}')\n if isinstance(d, dict) and 'name' in d:\n cfgs.append(d)\n except (ValueError, SyntaxError):\n continue\n\n return cfgs\n\n\ndef _backup_with_name(model_names, platform_ids):\n \"\"\"按 mykey+模型名+机器人名 格式备份旧 mykey.py\"\"\"\n parts = ['mykey']\n for m in model_names[:3]:\n parts.append(m.replace('/', '-').replace('\\\\', '-'))\n for pid in platform_ids:\n pid_clean = pid.replace('_', '')\n if pid_clean not in parts:\n parts.append(pid_clean)\n safe_name = '_'.join(parts)\n if safe_name == 'mykey':\n safe_name = 'mykey_backup' # 避免和源文件同名\n if len(safe_name) > 100:\n safe_name = safe_name[:100]\n backup_path = os.path.join(PROJECT_ROOT, f'{safe_name}.py')\n shutil.copy2(MYKPY_PATH, backup_path)\n return backup_path\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# Main\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef main():\n banner()\n\n # Python 版本检查\n ok, msg = _check_python()\n if not ok:\n print(f\" {C['red']}✗ {msg}{C['reset']}\")\n sys.exit(1)\n if msg:\n color = 'yellow' if '⚠' in msg else 'green'\n print(f\" {C[color]}{msg}{C['reset']}\\n\")\n\n # ── 决策流程 ──\n llm_cfgs = []\n platform_configs = []\n platform_deps = set()\n is_modify = False\n is_new = False\n\n if os.path.exists(MYKPY_PATH):\n model_names, platform_infos = _parse_existing_mykey()\n cur_models = ', '.join(model_names) if model_names else '(未知)'\n cur_platforms = ', '.join(p['id'] for p in platform_infos) if platform_infos else '(无)'\n print(f\" {C['dim']} 当前: 模型=[{cur_models}], 平台=[{cur_platforms}]{C['reset']}\")\n\n mode = ask_choice(\n \"检测到已有 mykey.py,请选择操作\",\n [\n {'id': 'modify', 'name': '修改现有配置', 'desc': '保留未改部分,只重新配置选定项'},\n {'id': 'new', 'name': '新建配置(备份旧文件)', 'desc': '备份为 mykey+模型+平台.py,然后全新配置'},\n ],\n default=None,\n )\n\n if mode == 'new':\n backup_path = _backup_with_name(model_names, [p['id'] for p in platform_infos])\n print(f\" {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup_path}{C['reset']}\")\n is_new = True\n else:\n is_modify = True\n scope = ask_choice(\n \"你要修改什么?\",\n [\n {'id': 'both', 'name': '两项都重新配置', 'desc': 'LLM + 平台全部更新'},\n {'id': 'llm', 'name': '重新配置 LLM 模型', 'desc': f'当前: {cur_models}'},\n {'id': 'platform', 'name': '重新配置消息平台', 'desc': f'当前: {cur_platforms}'},\n ],\n )\n if scope in ('llm', 'both'):\n llm_cfgs = _do_llm()\n if scope in ('platform', 'both'):\n platform_configs, platform_deps = configure_platforms()\n if scope == 'llm' and platform_infos:\n for pi in platform_infos:\n p = next((x for x in PLATFORMS if x['id'] == pi['id']), None)\n if p:\n config_dict = {v['key']: v['val'] for v in pi['vars']}\n platform_configs.append({'platform': p, 'config': config_dict})\n elif scope == 'platform' and model_names:\n old_cfgs = _parse_existing_llm_cfgs()\n if old_cfgs:\n llm_cfgs = old_cfgs\n print(f\"\\n {C['green']}✓ 已保留现有 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}\")\n else:\n print(f\"\\n {C['yellow']}⚠ 保留 LLM 配置失败,将生成空配置。建议两项都重新配置。{C['reset']}\")\n\n if not is_modify:\n if is_new:\n hint = \"已备份旧配置,请完成全新设置\"\n else:\n hint = \"首次配置,建议同时设置模型和消息平台\"\n print(f\" {C['dim']} {hint}。{C['reset']}\")\n\n scope = ask_choice(\n \"你想配置什么?\",\n [\n {'id': 'both', 'name': '两项都配置 (推荐)', 'desc': 'LLM 模型 + 消息平台,完整初始化'},\n {'id': 'llm', 'name': '仅 LLM 模型', 'desc': '只配置模型,稍后再配平台'},\n {'id': 'platform', 'name': '仅消息平台', 'desc': '只配平台,稍后再配模型'},\n ],\n default='both',\n )\n\n if scope in ('llm', 'both'):\n llm_cfgs = _do_llm()\n if scope == 'llm':\n if ask_yesno(\"是否继续配置消息平台?\", default=True):\n platform_configs, platform_deps = configure_platforms()\n\n if scope == 'both':\n platform_configs, platform_deps = configure_platforms()\n\n if scope == 'platform':\n platform_configs, platform_deps = configure_platforms()\n if ask_yesno(\"是否继续配置 LLM 模型?\", default=True):\n llm_cfgs = _do_llm()\n elif os.path.exists(MYKPY_PATH):\n # 新建+仅平台:从备份保留旧 LLM 配置\n old_cfgs = _parse_existing_llm_cfgs()\n if old_cfgs:\n llm_cfgs = old_cfgs\n print(f\"\\n {C['green']}✓ 已保留备份中的 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}\")\n\n # ── 生成 mykey.py ──\n if not llm_cfgs and not platform_configs:\n print(f\"\\n {C['yellow']}⚠ 没有配置任何内容,退出。{C['reset']}\")\n sys.exit(0)\n\n content = generate_mykey(llm_cfgs, platform_configs)\n\n # 备份旧文件(修改模式不备份,直接在原文件修改)\n if os.path.exists(MYKPY_PATH) and not is_modify and not is_new:\n backup = _backup_with_name(model_names, [p['id'] for p in platform_infos])\n print(f\"\\n {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}\")\n\n # 写入\n with open(MYKPY_PATH, 'w', encoding='utf-8') as f:\n f.write(content)\n print(f\"\\n {C['green']}✓ mykey.py 已生成!{C['reset']}\")\n\n # ── 完成提示 ──\n print(f\"\\n{C['bold']}{C['green']}╔══════════════════════════════════════╗\")\n print(f\"║ 配置完成! ║\")\n print(f\"╚══════════════════════════════════════╝{C['reset']}\")\n print()\n if llm_cfgs:\n print(f\" {C['cyan']} 终端 REPL:{C['reset']} python agentmain.py\")\n if platform_configs:\n for i, pc in enumerate(platform_configs, 1):\n p = pc['platform']\n print(f\" {C['cyan']} 平台 {i} ({p['name']}):{C['reset']} python {p['file']}\")\n print()\n\n # pip 依赖提示\n all_deps = sorted(platform_deps)\n if all_deps:\n print(f\" {C['yellow']}💡 提示:你需要安装以下依赖以使消息平台正常工作:{C['reset']}\")\n print(f\" {C['cyan']}pip install {' '.join(all_deps)}{C['reset']}\")\n print()\n\n # ── 入门示例 ──\n print(f\" {C['bold']}试试这些命令:{C['reset']}\")\n examples = [\n \"帮我在桌面创建一个 hello.txt,内容是 Hello World\",\n \"请查看你的代码,安装所有用得上的 python 依赖\",\n \"执行 web setup sop,解锁 web 工具\",\n \"打开淘宝,搜索 iPhone 16,按价格排序\",\n \"用rapidocr配置你的ocr能力并存入记忆\",\n \"git 更新你的代码,然后看看 commit 有什么新功能\",\n \"把这个记到你的记忆里\",\n ]\n for ex in examples:\n print(f\" {C['dim']}{ex}{C['reset']}\")\n print()\n\n print(f\" {C['green']}{C['bold']}合抱之木,生于毫末{C['reset']}\\n\")\n\n\ndef _do_llm():\n \"\"\"配置 LLM 模型,失败则 exit。\"\"\"\n cfgs = configure_llms()\n if not cfgs:\n print(f\"\\n {C['red']}✗ 至少需要配置一个模型才能使用。退出。{C['reset']}\")\n sys.exit(1)\n return cfgs\n\n\nif __name__ == '__main__':\n try:\n main()\n except KeyboardInterrupt:\n print(f\"\\n\\n {C['yellow']}⚠ 用户中断{C['reset']}\")\n sys.exit(0)\n"}, "00fd5f81c09ee25c80e0c87099f993c9019032ee": {"t": 1785763790.7598093, "n": 54456, "h": "121de44c6cb7c84c", "c": "#!/usr/bin/env python3\n\"\"\"\nGenericAgent — 交互式初始化向导 (configure.py)\n一键配置 LLM 模型 + 消息平台,自动生成 mykey.py\n\n用法:\n python configure.py\n\"\"\"\n\nimport ast\nimport os\nimport sys\nimport re\nimport shutil\nimport json\nimport urllib.request\nfrom datetime import datetime\n\n# ── ANSI 颜色 ──────────────────────────────────────────────────────────────\nC = {\n 'reset': '\\033[0m', 'bold': '\\033[1m', 'dim': '\\033[2m',\n 'red': '\\033[91m', 'green': '\\033[92m', 'yellow': '\\033[93m',\n 'blue': '\\033[94m', 'magenta': '\\033[95m', 'cyan': '\\033[96m', 'white': '\\033[97m',\n}\n\nPROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\nMYKPY_PATH = os.path.join(PROJECT_ROOT, 'mykey.py')\n\n# ── 模型厂商定义 ───────────────────────────────────────────────────────────\n\nLLM_PROVIDERS = [\n # ═══════════════════════════ 通用协议(官方直连或任意兼容中转)═══════════════════════════\n {\n 'id': 'oai_chat',\n 'name': 'OpenAI Chat Completions 协议',\n 'desc': '官方直连或任意 OAI 兼容中转/网关,自填 apibase(回车=OpenAI 官方)',\n 'type': 'native_oai',\n 'template': {\n 'name': 'gpt-native', 'apikey': 'sk-<your-key>',\n 'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.5',\n 'api_mode': 'chat_completions', 'reasoning_effort': 'high',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '官方在 https://platform.openai.com/api-keys 获取;中转站填其提供的 Key',\n 'model_choices': ['gpt-5.5', 'gpt-5.4'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.openai.com/v1'},\n ],\n },\n {\n 'id': 'oai_responses',\n 'name': 'OpenAI Responses 协议',\n 'desc': 'Responses API(o 系列/GPT-5.5 推荐端点),官方或兼容网关,自填 apibase',\n 'type': 'native_oai',\n 'template': {\n 'name': 'gpt-responses', 'apikey': 'sk-<your-key>',\n 'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.5',\n 'api_mode': 'responses', 'reasoning_effort': 'high',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '官方在 https://platform.openai.com/api-keys 获取;中转站填其提供的 Key',\n 'model_choices': ['gpt-5.5', 'gpt-5.4'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.openai.com/v1'},\n ],\n },\n {\n 'id': 'claude_messages',\n 'name': 'Claude Messages 协议',\n 'desc': 'Anthropic 官方直连或任意 Claude 兼容中转,自填 apibase(回车=官方)',\n 'type': 'native_claude',\n 'template': {\n 'name': 'anthropic-direct', 'apikey': 'sk-ant-<your-key>',\n 'apibase': 'https://api.anthropic.com', 'model': 'claude-opus-4-7',\n 'thinking_type': 'adaptive', 'max_tokens': 32768, 'temperature': 1,\n },\n 'key_hint': '官方在 https://console.anthropic.com/ 获取;中转站填其提供的 Key',\n 'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.anthropic.com'},\n ],\n },\n # ═══════════════════════════ 直连 API(按旗舰能力降序)═══════════════════════════\n {\n 'id': 'deepseek',\n 'name': 'DeepSeek (v4-Pro / Flash)',\n 'desc': '开源模型,v4-Pro 旗舰 1M 上下文',\n 'type': 'native_oai',\n 'template': {\n 'name': 'deepseek', 'apikey': 'sk-<your-deepseek-key>',\n 'apibase': 'https://api.deepseek.com', 'model': 'deepseek-v4-flash',\n 'api_mode': 'chat_completions', 'reasoning_effort': 'high',\n },\n 'key_hint': '在 https://platform.deepseek.com/api_keys 获取',\n 'model_choices': ['deepseek-v4-pro', 'deepseek-v4-flash'],\n },\n {\n 'id': 'kimi',\n 'name': 'Kimi (k2.6 / k2.5) 双协议',\n 'desc': '月之暗面,支持 Anthropic 和 OAI 双协议',\n 'type': 'native_claude',\n 'template': {\n 'name': 'kimi', 'apikey': 'sk-kimi-<your-key>',\n 'apibase': 'https://api.kimi.com/coding',\n 'model': 'kimi-for-coding', 'fake_cc_system_prompt': True,\n 'thinking_type': 'adaptive',\n },\n 'key_hint': '在 https://kimi.com/code 或 https://platform.moonshot.cn/ 获取',\n 'model_choices': ['kimi-k2.6', 'kimi-k2.5'],\n 'extra_fields': [\n {\n 'key': '_protocol', 'label': '选择 API 协议',\n 'type': 'choice',\n 'options': [\n {'id': 'native_claude', 'name': 'Anthropic 兼容 (推荐)', 'desc': 'kimi-for-coding 端点,CC 兼容', 'apibase': 'https://api.kimi.com/coding', 'fake_cc_system_prompt': True, 'model': 'kimi-for-coding'},\n {'id': 'native_oai', 'name': 'OpenAI 协议', 'desc': 'Moonshot OAI 端点,kimi-k2 系列', 'apibase': 'https://api.moonshot.cn/v1', 'model': 'kimi-k2.6'},\n ],\n },\n ],\n },\n {\n 'id': 'qwen',\n 'name': '阿里通义千问 (Qwen3.5 / 百炼)',\n 'desc': '阿里云百炼,Qwen3 系列百万级上下文',\n 'type': 'native_oai',\n 'template': {\n 'name': 'qwen', 'apikey': 'sk-<your-dashscope-key>',\n 'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n 'model': 'qwen3.5-plus',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://bailian.console.aliyun.com/ 获取 API Key',\n 'model_choices': ['qwen3.5-plus', 'qwen3-coder-plus'],\n 'extra_fields': [\n {\n 'key': '_endpoint', 'label': '选择端点',\n 'type': 'choice',\n 'options': [\n {'id': 'standard', 'name': '标准按量付费', 'desc': 'dashscope.aliyuncs.com,兼容模式', 'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1'},\n {'id': 'coding_plan', 'name': '百炼 Coding Plan (订阅)', 'desc': 'coding-intl.dashscope.aliyuncs.com,100万上下文', 'apibase': 'https://coding-intl.dashscope.aliyuncs.com/v1', 'context_win': 1000000},\n ],\n },\n ],\n },\n {\n 'id': 'zhipu',\n 'name': '智谱 GLM-5.1 (Coding Plan)',\n 'desc': '智谱 GLM,支持 Coding Plan CN (Anthropic) 和 Global (OAI) 双端点',\n 'type': 'native_claude',\n 'template': {\n 'name': 'zhipu-glm', 'apikey': 'sk-<your-zhipu-key>',\n 'apibase': 'https://open.bigmodel.cn/api/anthropic',\n 'model': 'GLM-5.1-Cloud', 'fake_cc_system_prompt': False,\n 'thinking_type': 'adaptive', 'max_retries': 3,\n 'connect_timeout': 10, 'read_timeout': 180,\n },\n 'key_hint': 'CN 在 https://open.bigmodel.cn/ 获取;Global 在 https://z.ai/ 获取',\n 'model_choices': ['GLM-5.1-Cloud', 'glm-4.7'],\n 'extra_fields': [\n {\n 'key': '_plan', 'label': '选择 Coding Plan',\n 'type': 'choice',\n 'options': [\n {'id': 'native_claude', 'name': 'Coding Plan CN (Anthropic)', 'desc': 'open.bigmodel.cn,推荐国内用户', 'apibase': 'https://open.bigmodel.cn/api/anthropic', 'fake_cc_system_prompt': False},\n {'id': 'native_oai', 'name': 'Coding Plan Global (OAI)', 'desc': 'api.z.ai,OpenAI 协议,全球可用', 'apibase': 'https://api.z.ai/api/paas/v4'},\n ],\n },\n ],\n },\n {\n 'id': 'minimax',\n 'name': 'MiniMax M3 (双协议)',\n 'desc': 'MiniMax M3,支持 Anthropic 和 OpenAI 双协议',\n 'type': 'native_claude',\n 'template': {\n 'name': 'minimax', 'apikey': 'eyJh...<your-minimax-key>',\n 'apibase': 'https://api.minimaxi.com/anthropic',\n 'model': 'MiniMax-M3', 'max_retries': 3,\n },\n 'key_hint': '在 https://platform.minimaxi.com/user-center/basic-information 获取',\n 'model_choices': ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],\n 'extra_fields': [\n {\n 'key': '_protocol', 'label': '选择 API 协议',\n 'type': 'choice',\n 'options': [\n {'id': 'native_claude', 'name': 'Anthropic 协议 (推荐)', 'desc': '无 <think> 标签,原生 Claude 兼容', 'apibase': 'https://api.minimaxi.com/anthropic'},\n {'id': 'native_oai', 'name': 'OpenAI 协议', 'desc': '走 /v1/chat/completions', 'apibase': 'https://api.minimaxi.com/v1', 'context_win': 50000},\n ],\n },\n ],\n },\n {\n 'id': 'stepfun',\n 'name': '阶跃星辰 Step-3.5 (推理强)',\n 'desc': '阶跃星辰 Step 系列,支持标准和 Step Plan 双端点',\n 'type': 'native_oai',\n 'template': {\n 'name': 'stepfun', 'apikey': 'sk-<your-stepfun-key>',\n 'apibase': 'https://api.stepfun.com/v1',\n 'model': 'step-3.5-flash',\n 'api_mode': 'chat_completions',\n 'context_win': 262144,\n },\n 'key_hint': '在 https://platform.stepfun.com/ 获取 API Key',\n 'model_choices': ['step-3.5-flash', 'step-3.5-flash-2603'],\n 'extra_fields': [\n {\n 'key': '_endpoint', 'label': '选择端点',\n 'type': 'choice',\n 'options': [\n {'id': 'standard', 'name': '标准端点', 'desc': 'api.stepfun.com/v1,按量付费', 'apibase': 'https://api.stepfun.com/v1', 'context_win': 262144},\n {'id': 'step_plan', 'name': 'Step Plan (订阅)', 'desc': 'api.stepfun.com/step_plan/v1,订阅制', 'apibase': 'https://api.stepfun.com/step_plan/v1', 'context_win': 262144},\n ],\n },\n ],\n },\n {\n 'id': 'qianfan',\n 'name': '百度千帆 (ERNIE 5.0 / 第三方)',\n 'desc': '百度智能云千帆,文心一言 ERNIE 5.0 + DeepSeek 等',\n 'type': 'native_oai',\n 'template': {\n 'name': 'baidu-qianfan', 'apikey': '<your-qianfan-key>',\n 'apibase': 'https://qianfan.baidubce.com/v2',\n 'model': 'ernie-5.0-thinking-preview',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://console.bce.baidu.com/qianfan/ 创建应用获取 API Key',\n 'model_choices': ['ernie-5.0-thinking-preview', 'deepseek-v3.2'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://qianfan.baidubce.com/v2'},\n ],\n },\n {\n 'id': 'volcengine',\n 'name': '火山引擎 (豆包 / Ark)',\n 'desc': '字节跳动火山引擎,支持标准 Ark 和 Ark Coding Plan',\n 'type': 'native_oai',\n 'template': {\n 'name': 'volc-ark', 'apikey': '<your-ark-api-key>',\n 'apibase': 'https://ark.cn-beijing.volces.com/api/v3',\n 'model': 'doubao-seed-code-preview-251028',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://console.volcengine.com/ark/ 创建推理接入点后获取 API Key',\n 'model_choices': ['doubao-seed-code-preview-251028', 'doubao-seed-1-8-251228'],\n 'extra_fields': [\n {\n 'key': '_endpoint', 'label': '选择端点',\n 'type': 'choice',\n 'options': [\n {'id': 'standard', 'name': '标准 Ark', 'desc': 'ark.cn-beijing.volces.com/api/v3,按量付费', 'apibase': 'https://ark.cn-beijing.volces.com/api/v3'},\n {'id': 'coding_plan', 'name': 'Ark Coding Plan (订阅)', 'desc': 'ark.cn-beijing.volces.com/api/coding/v3', 'apibase': 'https://ark.cn-beijing.volces.com/api/coding/v3'},\n ],\n },\n ],\n },\n {\n 'id': 'xiaomi',\n 'name': '小米 MiMo (MiMo 2.5 Pro / TokenPlan)',\n 'desc': '小米 MiMo 系列,超大上下文窗口,支持 TokenPlan 预付费',\n 'type': 'native_oai',\n 'template': {\n 'name': 'xiaomi-mimo', 'apikey': 'sk-<your-xiaomi-key>',\n 'apibase': 'https://api.xiaomimimo.com/v1',\n 'model': 'mimo-v2.5-pro',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://x.xiaomi.com/ 获取 API Key',\n 'model_choices': ['mimo-v2.5-pro', 'mimo-v2-flash'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://api.xiaomimimo.com/v1'},\n ],\n },\n {\n 'id': 'tencent_tokenhub',\n 'name': '腾讯混元 TokenHub (Hy3 / TokenPlan)',\n 'desc': '腾讯云 TokenHub,混元 Hy3 系列,TokenPlan 预付费',\n 'type': 'native_oai',\n 'template': {\n 'name': 'tencent-tokenhub', 'apikey': 'sk-<your-tokenhub-key>',\n 'apibase': 'https://tokenhub.tencentmaas.com/v1',\n 'model': 'hy3-preview',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '在 https://console.cloud.tencent.com/tokenhub 获取 API Key',\n 'model_choices': ['hy3-preview'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://tokenhub.tencentmaas.com/v1'},\n ],\n },\n # ═══════════════════════════ 代理 / 中继(支持 Claude/GPT 等顶级模型)══════════\n {\n 'id': 'cc_relay',\n 'name': 'CC Switch 透传 (社区常用)',\n 'desc': '社区 Claude Code 透传渠道,可接入 Claude Opus',\n 'type': 'native_claude',\n 'template': {\n 'name': 'cc-relay', 'apikey': 'sk-user-<your-relay-key>',\n 'apibase': 'https://<your-cc-switch-host>/claude/office',\n 'model': 'claude-opus-4-7', 'fake_cc_system_prompt': True,\n 'thinking_type': 'adaptive',\n },\n 'key_hint': '从你的 CC Switch 服务商获取 apikey 和 apibase',\n 'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'],\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://your-host/claude/office'},\n {'key': 'fake_cc_system_prompt', 'label': 'fake_cc_system_prompt', 'type': 'bool', 'default': True},\n ],\n },\n {\n 'id': 'openrouter',\n 'name': 'OpenRouter (多模型中继)',\n 'desc': '一个 Key 通吃 Claude/GPT/DeepSeek/Qwen 等',\n 'type': 'native_oai',\n 'template': {\n 'name': 'openrouter', 'apikey': 'sk-or-<your-openrouter-key>',\n 'apibase': 'https://openrouter.ai/api/v1',\n 'model': 'anthropic/claude-opus-4-7',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '在 https://openrouter.ai/keys 获取',\n 'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.5'],\n },\n {\n 'id': 'commonstack',\n 'name': 'CommonStack (统一网关)',\n 'desc': '一个 Key 通吃 Claude/GPT/Gemini/DeepSeek/MiniMax/Zhipu/xAI 等',\n 'type': 'native_oai',\n 'template': {\n 'name': 'commonstack', 'apikey': 'sk-<your-commonstack-key>',\n 'apibase': 'https://api.commonstack.ai/v1',\n 'model': 'anthropic/claude-opus-4-7',\n 'api_mode': 'chat_completions',\n 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,\n },\n 'key_hint': '在 https://commonstack.ai 注册后从 Dashboard 获取 API Key',\n 'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.5'],\n },\n {\n 'id': 'crs',\n 'name': 'CRS 反代 (Claude Max 多通道)',\n 'desc': 'CRS 协议的反代服务,支持 Claude Max / Gemini Ultra 通道',\n 'type': 'native_claude',\n 'template': {\n 'name': 'crs', 'apikey': 'cr_<your-crs-key>',\n 'apibase': 'https://<your-crs-host>/api',\n 'model': 'claude-opus-4-7[1m]', 'fake_cc_system_prompt': True,\n 'thinking_type': 'adaptive', 'max_tokens': 32768,\n 'max_retries': 3, 'read_timeout': 180,\n },\n 'key_hint': '从你的 CRS 服务商获取 key 和 host',\n 'model_choices': ['claude-opus-4-7[1m]', 'claude-sonnet-4-6'],\n 'extra_fields': [\n {\n 'key': '_channel', 'label': '选择 CRS 通道',\n 'type': 'choice',\n 'options': [\n {'id': 'claude_max', 'name': 'Claude Max (默认)', 'desc': '标准 CRS Claude 通道', 'apibase': 'https://<your-crs-host>/api'},\n {'id': 'gemini_ultra', 'name': 'Gemini Ultra (Antigravity)', 'desc': 'CRS 包装的 Google Antigravity,不支持 SSE 流式', 'apibase': 'https://<your-crs-gemini-host>/antigravity/api', 'model': 'claude-opus-4-7-thinking', 'stream': False},\n ],\n },\n ],\n },\n {\n 'id': 'gmi',\n 'name': 'GMI Serving (通用模型中继)',\n 'desc': 'GMI 通用模型推理服务,支持多种开源/闭源(手动输入模型名)',\n 'type': 'native_oai',\n 'template': {\n 'name': 'gmi', 'apikey': '<your-gmi-key>',\n 'apibase': 'https://api.gmi-serving.com/v1',\n 'model': 'gmi-default',\n 'api_mode': 'chat_completions',\n },\n 'key_hint': '从 GMI 服务商获取 API Key,探测失败时手动输入模型名',\n 'model_choices': [], # 中继服务,模型由服务商提供,探测失败时手动输入\n 'extra_fields': [\n {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://api.gmi-serving.com/v1'},\n ],\n },\n]\n\n# ── 消息平台定义 ────────────────────────────────────────────────────────────\nPLATFORMS = [\n {\n 'id': 'none',\n 'name': '不使用消息平台(纯终端 REPL)',\n 'desc': '直接用 python agentmain.py 在终端交互',\n 'deps': [],\n },\n {\n 'id': 'telegram',\n 'name': 'Telegram 机器人',\n 'desc': '通过 Telegram Bot 与 Agent 对话',\n 'file': 'frontends/tgapp.py',\n 'deps': ['python-telegram-bot'],\n 'env_vars': [\n {'key': 'tg_bot_token', 'label': 'Bot Token', 'hint': '从 @BotFather 获取'},\n {'key': 'tg_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'qq',\n 'name': 'QQ 机器人',\n 'desc': '通过 QQ 官方机器人 API 接入',\n 'file': 'frontends/qqapp.py',\n 'deps': ['qq-botpy'],\n 'env_vars': [\n {'key': 'qq_app_id', 'label': 'App ID', 'hint': 'QQ 开放平台获取'},\n {'key': 'qq_app_secret', 'label': 'App Secret'},\n {'key': 'qq_allowed_users', 'label': '允许的用户 OpenID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'feishu',\n 'name': '飞书机器人',\n 'desc': '通过飞书应用与 Agent 对话',\n 'file': 'frontends/fsapp.py',\n 'deps': ['lark-oapi'],\n 'env_vars': [\n {'key': 'fs_app_id', 'label': 'App ID', 'hint': '飞书开放平台获取'},\n {'key': 'fs_app_secret', 'label': 'App Secret'},\n {'key': 'fs_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'wecom',\n 'name': '企业微信机器人',\n 'desc': '通过企业微信 Bot 接入',\n 'file': 'frontends/wecomapp.py',\n 'deps': ['wecombot'],\n 'env_vars': [\n {'key': 'wecom_bot_id', 'label': 'Bot ID'},\n {'key': 'wecom_secret', 'label': 'Bot Secret'},\n {'key': 'wecom_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'dingtalk',\n 'name': '钉钉机器人',\n 'desc': '通过钉钉应用接入',\n 'file': 'frontends/dingtalkapp.py',\n 'deps': ['dingtalk-sdk'],\n 'env_vars': [\n {'key': 'dingtalk_client_id', 'label': 'Client ID (App Key)'},\n {'key': 'dingtalk_client_secret', 'label': 'Client Secret (App Secret)'},\n {'key': 'dingtalk_allowed_users', 'label': '允许的用户 StaffID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'discord',\n 'name': 'Discord 机器人',\n 'desc': '通过 Discord Bot 接入',\n 'file': 'frontends/dcapp.py',\n 'deps': ['discord.py'],\n 'env_vars': [\n {'key': 'dc_bot_token', 'label': 'Bot Token', 'hint': 'Discord Developer Portal 获取'},\n {'key': 'dc_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},\n ],\n },\n {\n 'id': 'wechat',\n 'name': '微信 (iLink 协议)',\n 'desc': '通过微信个人号与 Agent 对话,扫码自动登录',\n 'file': 'frontends/wechatapp.py',\n 'deps': ['requests', 'qrcode', 'pycryptodome'],\n 'env_vars': [],\n },\n]\n\n\ndef _masked(v, reveal, tail):\n \"\"\"生成脱敏字符串:前 reveal 位明文 + * + 后 tail 位明文\"\"\"\n if len(v) > reveal + tail:\n return v[:reveal] + '*' * min(len(v) - reveal - tail, 8) + v[-tail:]\n elif len(v) > reveal:\n return v[:reveal] + '*' * (len(v) - reveal)\n return v\n\ndef masked_input(prompt, reveal=6, tail=4):\n \"\"\"密文输入,支持粘贴:批读取 + 延迟重绘,避免快速键入时丢字符。\n\n prompt 必须为单行(不含 \\\\n)。\n \"\"\"\n sys.stdout.write(prompt)\n sys.stdout.flush()\n chars = []\n\n def _repaint():\n m = _masked(''.join(chars), reveal, tail)\n sys.stdout.write(f'\\r{prompt}{m} \\r{prompt}{m}')\n sys.stdout.flush()\n\n def _process(c):\n \"\"\"处理单个字符,返回 True 表示应退出。\"\"\"\n if c in ('\\r', '\\n'):\n return True\n if c in ('\\x03', '\\x04'):\n raise KeyboardInterrupt\n if c in ('\\x08', '\\x7f'):\n if chars:\n chars.pop()\n elif c.isprintable() or c == ' ':\n chars.append(c)\n return False\n\n if os.name == 'nt':\n import msvcrt\n while True:\n c = msvcrt.getwch()\n if _process(c):\n break\n if c in ('\\x08', '\\x7f'):\n _repaint() # 退格立即重绘\n continue\n if not (c.isprintable() or c == ' '):\n continue\n # 批量读取:粘贴时一次取完\n while msvcrt.kbhit():\n c2 = msvcrt.getwch()\n if _process(c2):\n value = ''.join(chars)\n _repaint()\n sys.stdout.write('\\n')\n sys.stdout.flush()\n return value\n _repaint()\n else:\n import tty, termios, select\n fd = sys.stdin.fileno()\n old = termios.tcgetattr(fd)\n try:\n tty.setraw(fd)\n while True:\n c = sys.stdin.read(1)\n if _process(c):\n break\n if c in ('\\x08', '\\x7f'):\n _repaint() # 退格立即重绘\n continue\n if not (c.isprintable() or c == ' '):\n continue\n # 批量读取:只要 stdin 有数据就继续读,不重绘\n while select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):\n c2 = sys.stdin.read(1)\n if _process(c2):\n value = ''.join(chars)\n _repaint()\n termios.tcsetattr(fd, termios.TCSADRAIN, old)\n sys.stdout.write('\\n')\n sys.stdout.flush()\n return value\n _repaint()\n finally:\n termios.tcsetattr(fd, termios.TCSADRAIN, old)\n\n value = ''.join(chars)\n _repaint()\n sys.stdout.write('\\n')\n sys.stdout.flush()\n return value\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# UI Helpers\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef cprint(text, color=None, bold=False, end='\\n'):\n parts = []\n if color: parts.append(C.get(color, ''))\n if bold: parts.append(C['bold'])\n parts.append(text)\n parts.append(C['reset'])\n print(''.join(parts), end=end)\n\ndef banner():\n print('\\033[2J\\033[H', end='') # ANSI 清屏,跨平台\n print(f\"{C['cyan']}{C['bold']}\")\n print(\" ╔═══════════════════════════════════════════════════════════╗\")\n print(\" ║ GenericAgent — 交互式初始化向导 v1.2 ║\")\n print(\" ║ 一键配置 LLM 模型 + 消息平台,自动生成 mykey.py ║\")\n print(\" ╚═══════════════════════════════════════════════════════════╝\")\n print(f\"{C['reset']}\")\n print(f\"{C['dim']} 项目目录: {PROJECT_ROOT}{C['reset']}\")\n print()\n\ndef _check_python():\n \"\"\"检查 Python 版本,返回 (ok, msg)\"\"\"\n vi = sys.version_info\n if vi < (3, 10):\n return False, f\"Python {vi.major}.{vi.minor} 不满足最低要求 (≥ 3.10)\"\n if vi[:2] == (3, 12):\n return True, ''\n return True, f\"⚠ 当前 Python {vi.major}.{vi.minor},推荐使用 Python 3.12\"\n\ndef ask_choice(prompt, choices, allow_multi=False, default=None):\n \"\"\"交互式选择,返回 selected_id 或 [selected_ids]\"\"\"\n print(f\"\\n{C['bold']}{prompt}{C['reset']}\")\n if allow_multi:\n print(f\"{C['dim']} (可多选,输入序号用逗号分隔,如: 1,3,5;输入 a 全选;回车跳过){C['reset']}\")\n else:\n print(f\"{C['dim']} (输入序号,如: 1){C['reset']}\")\n for i, c in enumerate(choices, 1):\n desc = c.get('desc', '')\n print(f\" {C['green']}{i}.{C['reset']} {C['bold']}{c['name']}{C['reset']} {C['dim']}{desc}{C['reset']}\")\n while True:\n raw = input(f\"\\n {C['yellow']}►{C['reset']} \").strip()\n if not raw and default is not None:\n return default\n if allow_multi:\n if raw.lower() == 'a':\n return [c['id'] for c in choices]\n parts = [p.strip() for p in raw.split(',') if p.strip()]\n selected = []\n for p in parts:\n try:\n idx = int(p) - 1\n if 0 <= idx < len(choices):\n selected.append(choices[idx]['id'])\n except ValueError:\n pass\n if selected:\n return selected\n else:\n try:\n idx = int(raw) - 1\n if 0 <= idx < len(choices):\n return choices[idx]['id']\n except ValueError:\n pass\n print(f\" {C['red']}✗ 请输入有效序号{C['reset']}\")\n\ndef ask_input(prompt, default=None, secret=False, hint=None):\n \"\"\"交互式输入。secret=True 时使用脱敏输入。\"\"\"\n if hint:\n cprint(f\" {hint}\", 'dim')\n if default is not None:\n cprint(f\" [默认: {default}]\", 'dim')\n prompt_line = f\" {C['yellow']}►{C['reset']} {prompt}: \"\n while True:\n if secret:\n val = masked_input(prompt_line).strip()\n else:\n val = input(prompt_line).strip()\n if not val and default is not None:\n return default\n if val:\n return val\n cprint(\"✗ 此项不能为空\", 'red')\n\ndef ask_yesno(prompt, default=True):\n hint = \"Y/N\"\n raw = input(f\"\\n {C['yellow']}►{C['reset']} {prompt} ({hint}): \").strip().lower()\n if not raw:\n return default\n return raw.startswith('y')\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# LLM 配置逻辑\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef _get_proxy_handler():\n \"\"\"从环境变量读取代理配置,返回 ProxyHandler 或 None\"\"\"\n for var in ('HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'):\n url = os.environ.get(var)\n if url:\n return urllib.request.ProxyHandler({'https': url, 'http': url})\n return None\n\ndef probe_models(provider, apikey, apibase=None):\n \"\"\"调用 API 探测可用模型列表,返回模型 ID 列表或 None\"\"\"\n ptype = provider.get('type', 'native_oai')\n base = (apibase or provider['template'].get('apibase', '')).rstrip('/')\n\n if ptype == 'native_claude':\n url = f\"{base}/v1/models\"\n headers = {'x-api-key': apikey, 'anthropic-version': '2023-06-01', 'User-Agent': 'GenericAgent/1.0'}\n else:\n url = f\"{base}/models\"\n headers = {'Authorization': f'Bearer {apikey}', 'User-Agent': 'GenericAgent/1.0'}\n\n print(f\"\\n {C['dim']}🔍 正在探测可用模型 ({base}/models)...{C['reset']}\", end='', flush=True)\n if ptype == 'native_claude':\n print(f\" {C['dim']}(Anthropic 协议,探测可能失败){C['reset']}\", end='', flush=True)\n\n opener = urllib.request.build_opener()\n ph = _get_proxy_handler()\n if ph:\n opener = urllib.request.build_opener(ph)\n print(f\" {C['dim']}(via proxy){C['reset']}\", end='', flush=True)\n\n for attempt in range(2):\n try:\n req = urllib.request.Request(url, headers=headers, method='GET')\n with opener.open(req, timeout=10) as resp:\n data = json.loads(resp.read().decode())\n models = data.get('data', [])\n ids = sorted(set(m['id'] for m in models if isinstance(m, dict) and m.get('id')))\n if ids:\n print(f\" {C['green']}✓ 发现 {len(ids)} 个模型{C['reset']}\")\n return ids\n print(f\" {C['yellow']}⚠ 返回为空{C['reset']}\")\n return None\n except Exception as e:\n if attempt == 0 and 'timeout' in type(e).__name__.lower():\n print(f\" {C['yellow']}⏱ 超时,重试...{C['reset']}\", end='', flush=True)\n continue\n print(f\" {C['yellow']}⚠ 探测失败: {type(e).__name__}(将使用预设列表){C['reset']}\")\n return None\n return None\n\ndef _normalize_model_choices(choices):\n \"\"\"统一 model_choices 格式为 [{'id': str, 'name': str}]\"\"\"\n if not choices:\n return []\n result = []\n for item in choices:\n if isinstance(item, str):\n result.append({'id': item, 'name': item})\n elif isinstance(item, dict):\n result.append(item)\n elif isinstance(item, (tuple, list)) and len(item) >= 1:\n result.append({'id': item[0], 'name': item[1] if len(item) > 1 else item[0]})\n return result\n\ndef _configure_advanced(provider, cfg):\n \"\"\"配置高级可选字段: proxy, context_win, stream, user_agent, thinking_budget_tokens\"\"\"\n print(f\"\\n {C['dim']}── 高级选项(回车跳过,使用默认值){C['reset']}\")\n proxy = ask_input(\"HTTP 代理地址 (proxy)\", default='', hint='如 http://127.0.0.1:2082,留空跳过')\n if proxy:\n cfg['proxy'] = proxy\n cw = ask_input(\"上下文窗口阈值 (context_win)\", default='', hint='NativeClaude 默认 28000,其他默认 24000')\n if cw:\n cfg['context_win'] = int(cw)\n if cfg.get('thinking_type') == 'enabled':\n tbt = ask_input(\"thinking_budget_tokens\", default='', hint='low≈4096, medium≈10240, high≈32768')\n if tbt:\n cfg['thinking_budget_tokens'] = int(tbt)\n if cfg.get('type', provider['type']) == 'native_claude':\n ua = ask_input(\"User-Agent 版本号\", default='', hint='某些中转按 UA 白名单校验,pin 老版本用')\n if ua:\n cfg['user_agent'] = ua\n stream_default = cfg.get('stream', True)\n if ask_yesno(\"启用 SSE 流式 (stream)\", default=stream_default):\n cfg['stream'] = True\n else:\n cfg['stream'] = False\n\ndef configure_llm(provider):\n \"\"\"引导用户配置单个模型\"\"\"\n print(f\"\\n{C['cyan']}{'─'*60}{C['reset']}\")\n print(f\"{C['bold']} 配置: {provider['name']}{C['reset']}\")\n print(f\" {C['dim']}{provider['desc']}{C['reset']}\")\n print(f\"{C['cyan']}{'─'*60}{C['reset']}\")\n\n cfg = dict(provider['template'])\n\n # API Key(密文输入)\n cfg['apikey'] = ask_input(\n f\"API Key\",\n hint=provider.get('key_hint', ''),\n secret=True,\n )\n\n # 额外字段\n for field in provider.get('extra_fields', []):\n if field['key'] == 'apibase':\n cfg['apibase'] = ask_input(\n field['label'],\n default=field.get('default', cfg.get('apibase', '')),\n )\n elif field.get('type') == 'bool':\n cfg[field['key']] = ask_yesno(\n field['label'],\n default=field.get('default', True)\n )\n elif field.get('type') == 'choice':\n picked = ask_choice(field['label'], field['options'])\n chosen = next(o for o in field['options'] if o['id'] == picked)\n for opt_key, opt_val in chosen.items():\n if opt_key not in ('id', 'name', 'desc'):\n cfg[opt_key] = opt_val\n\n # 模型选择\n manual_choice = {'id': '__manual__', 'name': '✏️ 手动输入模型名', 'desc': '自定义模型 ID,不依赖探测结果'}\n model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase'))\n if model_list:\n refresh_choice = {'id': '__refresh__', 'name': '🔄 重新探测'}\n choices = [refresh_choice, manual_choice] + [{'id': m, 'name': m} for m in model_list]\n while True:\n picked = ask_choice(\"API 探测到以下可用模型(或手动输入):\", choices)\n if picked == '__refresh__':\n print(f\" {C['dim']}再次探测...{C['reset']}\")\n model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase'))\n if not model_list:\n print(f\" {C['yellow']}⚠ 再次探测失败{C['reset']}\")\n picked = _fallback_model(provider, manual_choice)\n break\n choices = [refresh_choice, manual_choice] + [{'id': m, 'name': m} for m in model_list]\n elif picked == '__manual__':\n picked = ask_input(\"请输入模型名\", default=cfg.get('model', ''))\n break\n else:\n break\n cfg['model'] = picked\n else:\n cfg['model'] = _fallback_model(provider, manual_choice)\n\n # 别名\n default_name = cfg.get('name', provider['id'])\n name = ask_input(\"此配置的别名 (name,Mixin 引用用)\", default=default_name)\n if name:\n cfg['name'] = name\n\n # 高级选项\n if ask_yesno(\"配置高级选项(proxy / context_win / stream 等)?\", default=False):\n _configure_advanced(provider, cfg)\n\n return cfg\n\ndef _fallback_model(provider, manual_choice=None):\n \"\"\"使用预设模型列表让用户选择,始终提供手动输入选项\"\"\"\n manual_choice = manual_choice or {'id': '__manual__', 'name': '✏️ 手动输入模型名', 'desc': '自定义模型 ID'}\n normalized = _normalize_model_choices(provider.get('model_choices', []))\n if normalized:\n choices = [manual_choice] + normalized\n picked = ask_choice(\"选择模型(或手动输入):\", choices)\n if picked == '__manual__':\n return ask_input(\"请输入模型名\", default=provider['template'].get('model', ''))\n return picked\n return ask_input(\"请输入模型名\", default=provider['template'].get('model', ''))\n\ndef configure_llms():\n \"\"\"配置 LLM 模型\"\"\"\n print(f\"\\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗\")\n print(f\"║ 第一步: 配置 LLM 模型 ║\")\n print(f\"╚══════════════════════════════════════╝{C['reset']}\")\n print(f\"\\n{C['dim']} 你可以配置最多 2 个模型组成故障转移 (Mixin) 列表。{C['reset']}\")\n\n all_cfgs = []\n provider_id = ask_choice(\"选择模型厂商 (配置第 1 个模型):\", LLM_PROVIDERS)\n provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id)\n cfg = configure_llm(provider)\n all_cfgs.append(cfg)\n\n if ask_yesno(\"再添加一个模型做故障转移?\", default=False):\n providers_ext = [{'id': '__stop__', 'name': '✓ 不需要备选了', 'desc': ''}] + LLM_PROVIDERS\n provider_id = ask_choice(\n \"选择模型厂商 (配置第 2 个模型 — 或选「不需要备选了」跳过):\",\n providers_ext\n )\n if provider_id != '__stop__':\n provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id)\n cfg = configure_llm(provider)\n all_cfgs.append(cfg)\n\n return all_cfgs\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# 消息平台配置逻辑\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef configure_platforms():\n \"\"\"配置消息平台,返回 (platform_configs, pip_hints)\"\"\"\n print(f\"\\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗\")\n print(f\"║ 第二步: 配置消息平台 ║\")\n print(f\"╚══════════════════════════════════════╝{C['reset']}\")\n print(f\"\\n{C['dim']} 消息平台用于从聊天软件与 Agent 交互。{C['reset']}\")\n print(f\"{C['dim']} 你也可以跳过此步,直接用终端 REPL。{C['reset']}\")\n\n platform_ids = ask_choice(\n \"选择消息平台 (可多选,选 '不使用' 则跳过):\",\n PLATFORMS,\n allow_multi=True,\n default=['none']\n )\n\n if 'none' in platform_ids:\n return [], set()\n\n selected_platforms = []\n pip_hints = set()\n\n for pid in platform_ids:\n platform = next(p for p in PLATFORMS if p['id'] == pid)\n pip_hints.update(platform.get('deps', []))\n\n print(f\"\\n{C['cyan']}{'─'*60}{C['reset']}\")\n print(f\"{C['bold']} 配置: {platform['name']}{C['reset']}\")\n print(f\"{C['cyan']}{'─'*60}{C['reset']}\")\n\n env_vals = {}\n\n if pid == 'feishu' and ask_yesno(\"使用一键扫码创建应用?(推荐)\", default=True):\n env_vals = _feishu_scan(platform)\n if pid == 'wechat' and ask_yesno(\"扫码登录微信 iLink?(推荐)\", default=True):\n env_vals = _wechat_scan()\n\n for var in platform['env_vars']:\n if var['key'] not in env_vals:\n env_vals.update(_manual_platform_var(var))\n\n if pid == 'wecom' and ask_yesno(\"设置欢迎消息?\", default=False):\n env_vals['wecom_welcome_message'] = ask_input(\"欢迎消息内容\", default='你好,我在线上。')\n\n selected_platforms.append({'platform': platform, 'config': env_vals})\n\n return selected_platforms, pip_hints\n\ndef _manual_platform_var(var):\n \"\"\"手动填写单个平台变量\"\"\"\n val = ask_input(var['label'], hint=var.get('hint', ''), default=var.get('default'))\n if var.get('is_list'):\n if val == '[]' or not val:\n return {var['key']: []}\n return {var['key']: [x.strip() for x in val.split(',') if x.strip()]}\n return {var['key']: val}\n\ndef _feishu_scan(platform):\n \"\"\"飞书一键扫码创建应用,返回 env_vals 或空 dict\"\"\"\n from io import StringIO\n try:\n import lark_oapi as lark\n import qrcode, threading\n except ImportError:\n print(f\"\\n {C['yellow']}⚠ lark-oapi 未安装,降级为手动配置{C['reset']}\")\n return {}\n\n print(f\"\\n {C['cyan']}📱 正在启动一键创建...{C['reset']}\")\n print(f\" {C['dim']} 请用飞书 App 扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\\n\")\n\n qr_printed = threading.Event()\n result_holder = {'data': None}\n\n def handle_qr(info):\n url = info['url']\n expire = info['expire_in']\n qr = qrcode.QRCode(border=1, box_size=1)\n qr.add_data(url)\n buf = StringIO()\n qr.print_ascii(out=buf)\n qr_art = buf.getvalue()\n print(f\"\\n {C['bold']}请用飞书扫描下方二维码,或复制链接在浏览器打开:{C['reset']}\")\n print(f\" {C['green']}{qr_art.replace(chr(27), '')}{C['reset']}\")\n print(f\" {C['dim']} 链接: {url}{C['reset']}\")\n print(f\" {C['dim']} 有效期 {expire} 秒{C['reset']}\")\n qr_printed.set()\n\n def handle_status(info):\n status = info['status']\n if status == 'polling':\n print(f\" {C['yellow']}⏳ 等待扫码...{C['reset']}\")\n elif status == 'slow_down':\n print(f\" {C['yellow']}⏳ 等待中... (间隔 {info.get('interval', '?')}s){C['reset']}\")\n elif status == 'domain_switched':\n print(f\" {C['cyan']}🌐 已切换认证域名{C['reset']}\")\n\n def run_register():\n try:\n result = lark.register_app(\n on_qr_code=handle_qr,\n on_status_change=handle_status,\n )\n result_holder['data'] = result\n except Exception as e:\n print(f\"\\n {C['red']}✗ 创建失败: {e}{C['reset']}\")\n\n thread = threading.Thread(target=run_register, daemon=True)\n thread.start()\n qr_printed.wait(timeout=15)\n thread.join(timeout=300)\n\n if result_holder['data']:\n result = result_holder['data']\n print(f\"\\n {C['green']}✅ 应用创建成功!{C['reset']}\")\n print(f\" App ID: {C['bold']}{result['client_id']}{C['reset']}\")\n print(f\" App Secret: {C['bold']}{result['client_secret']}{C['reset']}\")\n return {\n 'fs_app_id': result['client_id'],\n 'fs_app_secret': result['client_secret'],\n }\n else:\n print(f\"\\n {C['yellow']}⚠ 扫码创建未完成,降级为手动填写...{C['reset']}\")\n return {}\n\n\ndef _wechat_scan():\n \"\"\"微信 iLink 扫码登录,保存 token 到 ~/.wxbot/token.json,返回 env_vals\"\"\"\n print(f\"\\n {C['cyan']}📱 正在启动微信 iLink 扫码登录...{C['reset']}\")\n print(f\" {C['dim']} 请用微信扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\\n\")\n\n # 确保项目根在路径中,以便导入 frontends/wechatapp\n if PROJECT_ROOT not in sys.path:\n sys.path.insert(0, PROJECT_ROOT)\n try:\n from frontends.wechatapp import WxBotClient\n except ImportError as e:\n print(f\"\\n {C['yellow']}⚠ 无法导入 WxBotClient: {e}{C['reset']}\")\n return {}\n\n try:\n bot = WxBotClient()\n if bot.token:\n print(f\" {C['green']}✅ 已有有效 token (bot_id={bot.bot_id}){C['reset']}\")\n if ask_yesno(\"重新扫码登录?\", default=False):\n bot.token = ''\n else:\n return {}\n bot.login_qr()\n print(f\"\\n {C['green']}✅ 微信 iLink 扫码登录成功!{C['reset']}\")\n print(f\" Bot ID: {C['bold']}{bot.bot_id}{C['reset']}\")\n print(f\" Token 已保存到: {C['dim']}{bot._tf}{C['reset']}\")\n except Exception as e:\n print(f\"\\n {C['red']}✗ 扫码登录失败: {e}{C['reset']}\")\n return {}\n\n return {}\n\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# 生成 mykey.py\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef _var_type_info(cfg):\n \"\"\"根据配置类型返回 (var_prefix, session_type)\"\"\"\n cfg_type = cfg.get('type', 'native_oai')\n if cfg_type == 'native_claude':\n return 'native_claude_config', 'NativeClaudeSession'\n elif cfg_type == 'claude':\n return 'claude_config', 'ClaudeSession'\n elif cfg_type == 'oai':\n return 'oai_config', 'LLMSession'\n else:\n return 'native_oai_config', 'NativeOAISession'\n\n\ndef generate_mykey(llm_cfgs, platform_configs):\n \"\"\"生成 mykey.py 内容\"\"\"\n lines = []\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(f\"# GenericAgent — mykey.py (由 configure.py 自动生成 @ {datetime.now().strftime('%Y-%m-%d %H:%M')})\")\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"\")\n lines.append(\"# ── 停止符 ──────────────────────────────────────────────────────────────────\")\n lines.append(\"_SETUP_DONE = 'configure.py' # 删除此行可重新触发配置向导\")\n lines.append(\"\")\n\n # Mixin 配置\n names = [c['name'] for c in llm_cfgs]\n lines.append(\"# ── Mixin 故障转移 ──────────────────────────────────────────────────────────\")\n lines.append(\"mixin_config = {\")\n lines.append(f\" 'llm_nos': {names},\")\n lines.append(\" 'max_retries': 10,\")\n lines.append(\" 'base_delay': 0.5,\")\n lines.append(\"}\")\n lines.append(\"\")\n\n # 各模型配置\n type_counts = {}\n for cfg in llm_cfgs:\n cfg_type = cfg.get('type', 'native_oai')\n type_counts[cfg_type] = type_counts.get(cfg_type, 0) + 1\n\n type_indices = {}\n for i, cfg in enumerate(llm_cfgs):\n cfg_type = cfg.get('type', 'native_oai')\n var_prefix, session_type = _var_type_info(cfg)\n idx = type_indices.get(cfg_type, 0)\n type_indices[cfg_type] = idx + 1\n\n if type_counts[cfg_type] > 1:\n var_name = f\"{var_prefix}_{idx}\"\n else:\n var_name = var_prefix\n\n lines.append(f\"# ── {cfg['name']} ({session_type}) ─────────────────────────────────────────────\")\n lines.append(f\"{var_name} = {{\")\n _write_config_fields(lines, cfg)\n lines.append(\"}\")\n lines.append(\"\")\n\n # 平台配置\n if platform_configs:\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"# 聊天平台集成\")\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"\")\n for pc in platform_configs:\n for key, val in pc['config'].items():\n _write_platform_value(lines, key, val)\n lines.append(\"\")\n\n # 尾部\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n lines.append(\"# 配置完毕!运行: python agentmain.py (终端 REPL)\")\n if platform_configs:\n for pc in platform_configs:\n p = pc['platform']\n lines.append(f\"# 或: python {p['file']} ({p['name']})\")\n lines.append(\"# ══════════════════════════════════════════════════════════════════════════════\")\n\n return '\\n'.join(lines)\n\ndef _write_config_fields(lines, cfg):\n \"\"\"写入配置字典的键值对(缩进的 'key': value, 格式)\"\"\"\n for key in ['name', 'type', 'apikey', 'apibase', 'model', 'api_mode',\n 'fake_cc_system_prompt', 'thinking_type', 'thinking_budget_tokens',\n 'reasoning_effort', 'max_tokens', 'max_retries', 'connect_timeout',\n 'read_timeout', 'temperature', 'context_win',\n 'proxy', 'user_agent', 'stream']:\n if key not in cfg:\n continue\n val = cfg[key]\n if isinstance(val, bool):\n lines.append(f\" '{key}': {str(val)},\")\n elif isinstance(val, (int, float)):\n lines.append(f\" '{key}': {val},\")\n elif isinstance(val, str):\n lines.append(f\" '{key}': '{val}',\")\n else:\n lines.append(f\" '{key}': {repr(val)},\")\n\ndef _write_platform_value(lines, key, val):\n \"\"\"写入顶级变量(平台配置等)\"\"\"\n if isinstance(val, list):\n if val:\n lines.append(f\"{key} = {repr(val)}\")\n else:\n lines.append(f\"{key} = [] # 允许所有用户\")\n elif isinstance(val, str):\n lines.append(f\"{key} = '{val}'\")\n else:\n lines.append(f\"{key} = {repr(val)}\")\n\n\ndef _parse_existing_mykey():\n \"\"\"解析已有 mykey.py,返回 (model_names, platform_infos)\n\n model_names: [str] — 模型名列表\n platform_infos: [{'id': str, 'vars': [{'key': str, 'val': ...}]}] — 平台信息\n 解析失败时返回 ([], [])\n \"\"\"\n if not os.path.exists(MYKPY_PATH):\n return [], []\n\n with open(MYKPY_PATH, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # 解析模型名\n model_names = []\n m = re.search(r\"'llm_nos':\\s*\\[([^\\]]*)\\]\", content)\n if m:\n model_names = re.findall(r\"'([^']+)'\", m.group(1))\n\n # 先收集所有已知平台 env var key → 判断值类型\n all_env_var_keys = {}\n platform_env_keys = {} # pid -> [var_key]\n for p in PLATFORMS:\n pid = p['id']\n platform_env_keys.setdefault(pid, [])\n for var in p.get('env_vars', []):\n vkey = var['key']\n all_env_var_keys[vkey] = var\n platform_env_keys[pid].append(vkey)\n\n # 逐平台解析所有已知变量\n platform_infos = []\n for pid, env_keys in platform_env_keys.items():\n vars_found = []\n for vkey in env_keys:\n var_def = all_env_var_keys[vkey]\n val = None\n if var_def.get('is_list'):\n # 匹配 `xxx = [...]`\n m_var = re.search(rf\"^{vkey}\\s*=\\s*(\\[[^\\]]*\\])\", content, re.MULTILINE)\n if m_var:\n try:\n val = ast.literal_eval(m_var.group(1))\n except (ValueError, SyntaxError):\n pass\n else:\n # 匹配 `xxx = '...'`\n m_var = re.search(rf\"^{vkey}\\s*=\\s*'([^']*)'\", content, re.MULTILINE)\n if m_var:\n val = m_var.group(1)\n if val is not None:\n vars_found.append({'key': vkey, 'val': val})\n if vars_found:\n platform_infos.append({'id': pid, 'vars': vars_found})\n\n return model_names, platform_infos\n\n\ndef _parse_existing_llm_cfgs():\n \"\"\"解析已有 mykey.py,返回完整 LLM 配置字典列表 [{name, apikey, ...}]\n 解析失败时返回 []\n \"\"\"\n if not os.path.exists(MYKPY_PATH):\n return []\n\n with open(MYKPY_PATH, 'r', encoding='utf-8') as f:\n content = f.read()\n\n cfgs = []\n # 匹配所有 `xxx = { ... }` 顶层字典赋值\n # 用简单状态机: 找 `\\w+ = {` 然后匹配花括号\n pattern = re.compile(r'^(\\w+)\\s*=\\s*\\{', re.MULTILINE)\n for m in pattern.finditer(content):\n brace_start = m.end() - 1 # '{' 的位置\n depth = 1\n i = brace_start + 1\n while i < len(content) and depth > 0:\n if content[i] == '{':\n depth += 1\n elif content[i] == '}':\n depth -= 1\n i += 1\n if depth == 0:\n dict_text = content[m.end():i - 1]\n try:\n d = ast.literal_eval('{' + dict_text + '}')\n if isinstance(d, dict) and 'name' in d:\n cfgs.append(d)\n except (ValueError, SyntaxError):\n continue\n\n return cfgs\n\n\ndef _backup_with_name(model_names, platform_ids):\n \"\"\"按 mykey+模型名+机器人名 格式备份旧 mykey.py\"\"\"\n parts = ['mykey']\n for m in model_names[:3]:\n parts.append(m.replace('/', '-').replace('\\\\', '-'))\n for pid in platform_ids:\n pid_clean = pid.replace('_', '')\n if pid_clean not in parts:\n parts.append(pid_clean)\n safe_name = '_'.join(parts)\n if safe_name == 'mykey':\n safe_name = 'mykey_backup' # 避免和源文件同名\n if len(safe_name) > 100:\n safe_name = safe_name[:100]\n backup_path = os.path.join(PROJECT_ROOT, f'{safe_name}.py')\n shutil.copy2(MYKPY_PATH, backup_path)\n return backup_path\n\n\n# ═══════════════════════════════════════════════════════════════════════════\n# Main\n# ═══════════════════════════════════════════════════════════════════════════\n\ndef main():\n banner()\n\n # Python 版本检查\n ok, msg = _check_python()\n if not ok:\n print(f\" {C['red']}✗ {msg}{C['reset']}\")\n sys.exit(1)\n if msg:\n color = 'yellow' if '⚠' in msg else 'green'\n print(f\" {C[color]}{msg}{C['reset']}\\n\")\n\n # ── 决策流程 ──\n llm_cfgs = []\n platform_configs = []\n platform_deps = set()\n is_modify = False\n is_new = False\n\n if os.path.exists(MYKPY_PATH):\n model_names, platform_infos = _parse_existing_mykey()\n cur_models = ', '.join(model_names) if model_names else '(未知)'\n cur_platforms = ', '.join(p['id'] for p in platform_infos) if platform_infos else '(无)'\n print(f\" {C['dim']} 当前: 模型=[{cur_models}], 平台=[{cur_platforms}]{C['reset']}\")\n\n mode = ask_choice(\n \"检测到已有 mykey.py,请选择操作\",\n [\n {'id': 'modify', 'name': '修改现有配置', 'desc': '保留未改部分,只重新配置选定项'},\n {'id': 'new', 'name': '新建配置(备份旧文件)', 'desc': '备份为 mykey+模型+平台.py,然后全新配置'},\n ],\n default=None,\n )\n\n if mode == 'new':\n backup_path = _backup_with_name(model_names, [p['id'] for p in platform_infos])\n print(f\" {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup_path}{C['reset']}\")\n is_new = True\n else:\n is_modify = True\n scope = ask_choice(\n \"你要修改什么?\",\n [\n {'id': 'both', 'name': '两项都重新配置', 'desc': 'LLM + 平台全部更新'},\n {'id': 'llm', 'name': '重新配置 LLM 模型', 'desc': f'当前: {cur_models}'},\n {'id': 'platform', 'name': '重新配置消息平台', 'desc': f'当前: {cur_platforms}'},\n ],\n )\n if scope in ('llm', 'both'):\n llm_cfgs = _do_llm()\n if scope in ('platform', 'both'):\n platform_configs, platform_deps = configure_platforms()\n if scope == 'llm' and platform_infos:\n for pi in platform_infos:\n p = next((x for x in PLATFORMS if x['id'] == pi['id']), None)\n if p:\n config_dict = {v['key']: v['val'] for v in pi['vars']}\n platform_configs.append({'platform': p, 'config': config_dict})\n elif scope == 'platform' and model_names:\n old_cfgs = _parse_existing_llm_cfgs()\n if old_cfgs:\n llm_cfgs = old_cfgs\n print(f\"\\n {C['green']}✓ 已保留现有 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}\")\n else:\n print(f\"\\n {C['yellow']}⚠ 保留 LLM 配置失败,将生成空配置。建议两项都重新配置。{C['reset']}\")\n\n if not is_modify:\n if is_new:\n hint = \"已备份旧配置,请完成全新设置\"\n else:\n hint = \"首次配置,建议同时设置模型和消息平台\"\n print(f\" {C['dim']} {hint}。{C['reset']}\")\n\n scope = ask_choice(\n \"你想配置什么?\",\n [\n {'id': 'both', 'name': '两项都配置 (推荐)', 'desc': 'LLM 模型 + 消息平台,完整初始化'},\n {'id': 'llm', 'name': '仅 LLM 模型', 'desc': '只配置模型,稍后再配平台'},\n {'id': 'platform', 'name': '仅消息平台', 'desc': '只配平台,稍后再配模型'},\n ],\n default='both',\n )\n\n if scope in ('llm', 'both'):\n llm_cfgs = _do_llm()\n if scope == 'llm':\n if ask_yesno(\"是否继续配置消息平台?\", default=True):\n platform_configs, platform_deps = configure_platforms()\n\n if scope == 'both':\n platform_configs, platform_deps = configure_platforms()\n\n if scope == 'platform':\n platform_configs, platform_deps = configure_platforms()\n if ask_yesno(\"是否继续配置 LLM 模型?\", default=True):\n llm_cfgs = _do_llm()\n elif os.path.exists(MYKPY_PATH):\n # 新建+仅平台:从备份保留旧 LLM 配置\n old_cfgs = _parse_existing_llm_cfgs()\n if old_cfgs:\n llm_cfgs = old_cfgs\n print(f\"\\n {C['green']}✓ 已保留备份中的 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}\")\n\n # ── 生成 mykey.py ──\n if not llm_cfgs and not platform_configs:\n print(f\"\\n {C['yellow']}⚠ 没有配置任何内容,退出。{C['reset']}\")\n sys.exit(0)\n\n content = generate_mykey(llm_cfgs, platform_configs)\n\n # 备份旧文件(修改模式不备份,直接在原文件修改)\n if os.path.exists(MYKPY_PATH) and not is_modify and not is_new:\n backup = _backup_with_name(model_names, [p['id'] for p in platform_infos])\n print(f\"\\n {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}\")\n\n # 写入\n with open(MYKPY_PATH, 'w', encoding='utf-8') as f:\n f.write(content)\n print(f\"\\n {C['green']}✓ mykey.py 已生成!{C['reset']}\")\n\n # ── 完成提示 ──\n print(f\"\\n{C['bold']}{C['green']}╔══════════════════════════════════════╗\")\n print(f\"║ 配置完成! ║\")\n print(f\"╚══════════════════════════════════════╝{C['reset']}\")\n print()\n if llm_cfgs:\n print(f\" {C['cyan']} 终端 REPL:{C['reset']} python agentmain.py\")\n if platform_configs:\n for i, pc in enumerate(platform_configs, 1):\n p = pc['platform']\n print(f\" {C['cyan']} 平台 {i} ({p['name']}):{C['reset']} python {p['file']}\")\n print()\n\n # pip 依赖提示\n all_deps = sorted(platform_deps)\n if all_deps:\n print(f\" {C['yellow']}💡 提示:你需要安装以下依赖以使消息平台正常工作:{C['reset']}\")\n print(f\" {C['cyan']}pip install {' '.join(all_deps)}{C['reset']}\")\n print()\n\n # ── 入门示例 ──\n print(f\" {C['bold']}试试这些命令:{C['reset']}\")\n examples = [\n \"帮我在桌面创建一个 hello.txt,内容是 Hello World\",\n \"请查看你的代码,安装所有用得上的 python 依赖\",\n \"执行 web setup sop,解锁 web 工具\",\n \"打开淘宝,搜索 iPhone 16,按价格排序\",\n \"用rapidocr配置你的ocr能力并存入记忆\",\n \"git 更新你的代码,然后看看 commit 有什么新功能\",\n \"把这个记到你的记忆里\",\n ]\n for ex in examples:\n print(f\" {C['dim']}{ex}{C['reset']}\")\n print()\n\n print(f\" {C['green']}{C['bold']}合抱之木,生于毫末{C['reset']}\\n\")\n\n\ndef _do_llm():\n \"\"\"配置 LLM 模型,失败则 exit。\"\"\"\n cfgs = configure_llms()\n if not cfgs:\n print(f\"\\n {C['red']}✗ 至少需要配置一个模型才能使用。退出。{C['reset']}\")\n sys.exit(1)\n return cfgs\n\n\nif __name__ == '__main__':\n try:\n main()\n except KeyboardInterrupt:\n print(f\"\\n\\n {C['yellow']}⚠ 用户中断{C['reset']}\")\n sys.exit(0)\n"}}, "ga_core/mykey_template_en.py": {"eaa7b6c1c04eecdcb522f2dd2333ff37c2ccaaa3": {"t": 1785674428.20537, "n": 3419, "h": "c02311707403c367", "c": "# ══════════════════════════════════════════════════════════════════════════════\n# GenericAgent — mykey.py configuration template (copy to mykey.py and fill in)\n# ══════════════════════════════════════════════════════════════════════════════\n#\n# Quick start:\n# 1. Copy this file to mykey.py\n# 2. Uncomment one of the configs below and fill in your apikey\n# 3. Run `python agentmain.py` or `python launch.pyw`\n#\n# GA auto-detects any variable whose name contains 'api'/'config'/'cookie'\n# and picks the session class by keyword:\n# name contains 'native' + 'claude' → NativeClaudeSession (Anthropic API)\n# name contains 'native' + 'oai' → NativeOAISession (OpenAI API)\n# name contains 'mixin' → MixinSession (failover)\n#\n# Native = tools go in the API's native `tool` field (function calling), same\n# way Claude Code and Codex do it. Recommended for GPT / Claude / Gemini.\n#\n# Tip: runtime overrides via `/session.<attr>=<val>` in the REPL, e.g.\n# /session.reasoning_effort=high\n# /session.thinking_type=adaptive\n# /session.temperature=0.3\n#\n# ══════════════════════════════════════════════════════════════════════════════\n\n\n# ── 1. NativeClaudeSession — Anthropic direct ────────────────────────────────\n# Official Anthropic endpoint. apikey starting with 'sk-ant-' is auto-sent\n# as x-api-key; any other prefix uses Authorization: Bearer.\n# Model suffix '[1m]' triggers the 1M-context beta (stripped before sending).\nnative_claude_config = {\n 'name': 'claude', # display name & mixin reference\n 'apikey': 'sk-ant-<your-anthropic-key>',\n 'apibase': 'https://api.anthropic.com',\n 'model': 'claude-opus-4-7[1m]', # or 'claude-sonnet-4-6'\n 'thinking_type': 'adaptive', # 'adaptive' | 'enabled' | 'disabled'\n # 'thinking_budget_tokens': 32768, # required if thinking_type='enabled'\n # 'max_retries': 3,\n # 'read_timeout': 180,\n}\n\n\n# ── 2. NativeOAISession — OpenAI direct ──────────────────────────────────────\n# Standard OpenAI chat/completions endpoint. Also works for any OpenAI-\n# compatible provider that supports native function-calling tool fields.\nnative_oai_config = {\n 'name': 'gpt', # display name & mixin reference\n 'apikey': 'sk-<your-openai-key>',\n 'apibase': 'https://api.openai.com/v1',\n 'model': 'gpt-5.4', # or 'o4', 'gpt-5.3-codex', etc.\n 'api_mode': 'chat_completions', # or 'responses' for /v1/responses\n # 'reasoning_effort': 'high', # none|minimal|low|medium|high|xhigh\n # 'max_retries': 3,\n # 'read_timeout': 120,\n}\n\n\n# ── 3. Mixin failover (optional) ─────────────────────────────────────────────\n# List sessions by 'name'; if one fails, the next is tried automatically.\n# Constraint: all referenced sessions must be Native (mixing Native Claude\n# and Native OAI is fine; mixing Native with non-Native is not).\n# mixin_config = {\n# 'llm_nos': ['claude', 'gpt'],\n# 'max_retries': 5,\n# 'base_delay': 0.5,\n# }\n\n\n# ── 4. Global HTTP proxy (optional) ──────────────────────────────────────────\n# Applies to every session that doesn't set its own 'proxy' field.\n# proxy = 'http://127.0.0.1:7890'\n\n\n# ── 5. Chat platform integrations (optional) ─────────────────────────────────\n# tg_bot_token = '...'\n# tg_allowed_users = [123456789]\n"}}, "ga_core/llmcore.py": {"eaa7b6c1c04eecdcb522f2dd2333ff37c2ccaaa3": {"t": 1785674432.0525408, "n": 60964, "h": "21f4af4820d745fd", "c": "import os, json, re, time, requests, sys, threading, urllib3, base64, importlib, uuid, pathlib\nfrom datetime import datetime\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n_RESP_CACHE_KEY = str(uuid.uuid4()); _RESP_CODEX_KEY = str(uuid.uuid4())\n_ROOT = os.path.dirname(os.path.abspath(__file__))\nif _ROOT not in sys.path: sys.path.append(_ROOT)\n\ndef _load_mykeys():\n global _mykey_path\n try:\n import mykey; importlib.reload(mykey); _mykey_path = mykey.__file__\n return {k: v for k, v in vars(mykey).items() if not k.startswith('_')}\n except ImportError as e:\n if getattr(e, 'name', None) != 'mykey':\n raise Exception(f'[ERROR] mykey.py found but failed to import: {e}') from e\n except SyntaxError as e:\n raise Exception(f'[ERROR] mykey.py has syntax error: {e}') from e\n p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mykey.json')\n if not os.path.exists(p): raise Exception('[ERROR] mykey.py not found in sys.path and mykey.json not found. Run \"python configure_mykey.py\" or copy mykey_template.py to mykey.py and fill in your keys.')\n with open(_mykey_path := p, encoding='utf-8') as f: mk = json.load(f)\n if isinstance(mk, dict) and 'remote_url' in mk: return requests.get(mk['remote_url'], timeout=10).json()\n return mk\n\n_mykey_path = _mykey_mtime = None\ndef reload_mykeys():\n global _mykey_mtime\n try:\n mt = os.stat(_mykey_path).st_mtime_ns if _mykey_path else -1\n if mt == _mykey_mtime: return globals().get('mykeys', {}), False\n mk = _load_mykeys(); _mykey_mtime = os.stat(_mykey_path).st_mtime_ns\n print(f'[Info] Load mykeys from {_mykey_path}')\n globals().update(mykeys=mk)\n return mk, True\n except: return globals().get('mykeys', {}), False\n\ndef __getattr__(name): # once guard in PEP 562\n if name == 'mykeys': return reload_mykeys()[0]\n raise AttributeError(f\"module 'llmcore' has no attribute {name}\")\n\ndef compress_history_tags(messages, keep_recent=10, max_len=800, force=False, interval=5):\n \"\"\"Compress <thinking>/<tool_use>/<tool_result> tags in older messages to save tokens.\"\"\"\n compress_history_tags._cd = getattr(compress_history_tags, '_cd', 0) + 1\n if force: compress_history_tags._cd = 0\n if compress_history_tags._cd % interval != 0: return messages\n _before = sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)\n _pats = {tag: re.compile(rf'(<{tag}>)([\\s\\S]*?)(</{tag}>)') for tag in ('thinking', 'think', 'tool_use', 'tool_result')}\n _hist_pat = re.compile(r'<(history|key_info|earlier_context)>[\\s\\S]*?</\\1>')\n def _trunc_str(s): return s[:max_len//2] + '\\n...[Truncated]...\\n' + s[-max_len//2:] if isinstance(s, str) and len(s) > max_len else s\n def _trunc(text):\n text = _hist_pat.sub(lambda m: f'<{m.group(1)}>[...]</{m.group(1)}>', text)\n for pat in _pats.values(): text = pat.sub(lambda m: m.group(1) + _trunc_str(m.group(2)) + m.group(3), text)\n return text\n for i, msg in enumerate(messages):\n if i >= len(messages) - keep_recent: break\n c = msg['content']\n if isinstance(c, str): msg['content'] = _trunc(c)\n elif isinstance(c, list):\n for b in c:\n if not isinstance(b, dict): continue\n t = b.get('type')\n if t == 'text' and isinstance(b.get('text'), str): b['text'] = _trunc(b['text'])\n elif t == 'tool_result':\n tc = b.get('content')\n if isinstance(tc, str): b['content'] = _trunc_str(tc)\n elif isinstance(tc, list):\n for sub in tc:\n if isinstance(sub, dict) and sub.get('type') == 'text': sub['text'] = _trunc_str(sub.get('text'))\n elif t == 'tool_use' and isinstance(b.get('input'), dict):\n for k, v in b['input'].items(): b['input'][k] = _trunc_str(v)\n print(f\"[Cut] {_before} -> {sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)}\")\n return messages\n\ndef _sanitize_leading_user_msg(msg):\n \"\"\"把 user 消息里的 tool_result 块改写成纯文本,避免孤立引用。\n history 统一使用 Claude content-block 格式:content 是 list of blocks。\"\"\"\n msg = dict(msg) # 浅拷贝外层 dict\n content = msg.get('content')\n if not isinstance(content, list): return msg\n texts = []\n for block in content:\n if not isinstance(block, dict): continue\n if block.get('type') == 'tool_result':\n c = block.get('content', '')\n if isinstance(c, list): # content 本身也可能是 list[{type:text,text:...}]\n texts.extend(b.get('text', '') for b in c if isinstance(b, dict))\n else: texts.append(str(c))\n elif block.get('type') == 'text': texts.append(block.get('text', ''))\n msg['content'] = [{\"type\": \"text\", \"text\": '\\n'.join(t for t in texts if t)}]\n return msg\n\n_oldprint = print\ndef safeprint(*argv):\n try: _oldprint(*argv)\n except OSError: pass\nprint = safeprint\n\ndef trim_messages_history(history, sess):\n cap = sess.context_win * 3\n target = int(cap * getattr(sess, 'trim_keep_rate', 0.6))\n def cost(): return sum(len(json.dumps(m, ensure_ascii=False)) for m in history)\n compress_history_tags(history, interval=getattr(sess, 'cut_msg_interval', 5))\n print(f'[Debug] Current context: {cost()} chars, {len(history)} messages.')\n if cost() <= cap: return\n compress_history_tags(history, keep_recent=4, force=True)\n if cost() <= target: return\n while len(history) > 9 and cost() > target:\n history.pop(0)\n while history and history[0].get('role') != 'user': history.pop(0)\n if history and history[0].get('role') == 'user': history[0] = _sanitize_leading_user_msg(history[0])\n print(f'[Debug] Trimmed context, current: {cost()} chars, {len(history)} messages.')\n\ndef auto_make_url(base, path):\n b, p = base.rstrip('/'), path.strip('/')\n if b.endswith('$'): return b[:-1].rstrip('/')\n if b.endswith(p): return b\n return f\"{b}/{p}\" if re.search(r'/v\\d+(/|$)', b) else f\"{b}/v1/{p}\"\n\ndef _parse_claude_json(data):\n content_blocks = data.get(\"content\", [])\n _record_usage(data.get(\"usage\", {}), \"messages\")\n for b in content_blocks:\n if b.get(\"type\") == \"text\": yield b.get(\"text\", \"\")\n elif b.get(\"type\") == \"thinking\": yield \"\"\n return content_blocks\n\ndef _parse_claude_sse(resp_lines):\n \"\"\"Parse Anthropic SSE stream. Yields text chunks, returns list[content_block].\"\"\"\n content_blocks = []; current_block = None; tool_json_buf = \"\"\n stop_reason = None; got_message_stop = False; warn = None\n for line in resp_lines:\n if not line: continue\n line = line.decode('utf-8') if isinstance(line, bytes) else line\n if not line.startswith(\"data:\"): continue\n data_str = line[5:].lstrip()\n if data_str == \"[DONE]\": break\n try: evt = json.loads(data_str)\n except Exception as e:\n print(f\"[SSE] JSON parse error: {e}, line: {data_str[:200]}\")\n continue\n evt_type = evt.get(\"type\", \"\")\n if evt_type == \"message_start\":\n usage = evt.get(\"message\", {}).get(\"usage\", {})\n _record_usage(usage, \"messages\")\n elif evt_type == \"content_block_start\":\n block = evt.get(\"content_block\", {})\n if block.get(\"type\") == \"text\": current_block = {\"type\": \"text\", \"text\": \"\"}\n elif block.get(\"type\") == \"thinking\": current_block = {\"type\": \"thinking\", \"thinking\": \"\", \"signature\": \"\"}\n elif block.get(\"type\") == \"tool_use\":\n current_block = {\"type\": \"tool_use\", \"id\": block.get(\"id\", \"\"), \"name\": block.get(\"name\", \"\"), \"input\": {}}\n tool_json_buf = \"\"\n elif evt_type == \"content_block_delta\":\n delta = evt.get(\"delta\", {})\n if delta.get(\"type\") == \"text_delta\":\n text = delta.get(\"text\", \"\")\n if current_block and current_block.get(\"type\") == \"text\": current_block[\"text\"] += text\n if text: yield text\n elif delta.get(\"type\") == \"thinking_delta\":\n if current_block and current_block.get(\"type\") == \"thinking\": current_block[\"thinking\"] += delta.get(\"thinking\", \"\")\n elif delta.get(\"type\") == \"signature_delta\":\n if current_block and current_block.get(\"type\") == \"thinking\":\n current_block[\"signature\"] = current_block.get(\"signature\", \"\") + delta.get(\"signature\", \"\")\n elif delta.get(\"type\") == \"input_json_delta\": tool_json_buf += delta.get(\"partial_json\", \"\")\n elif evt_type == \"content_block_stop\":\n if current_block:\n if current_block[\"type\"] == \"tool_use\":\n try: current_block[\"input\"] = json.loads(tool_json_buf) if tool_json_buf else {}\n except: current_block[\"input\"] = {\"_raw\": tool_json_buf}\n content_blocks.append(current_block)\n current_block = None\n elif evt_type == \"message_delta\":\n delta = evt.get(\"delta\", {})\n stop_reason = delta.get(\"stop_reason\", stop_reason)\n out_usage = evt.get(\"usage\", {})\n out_tokens = out_usage.get(\"output_tokens\", 0)\n if out_tokens: print(f\"[Output] tokens={out_tokens} stop_reason={stop_reason}\")\n elif evt_type == \"message_stop\": got_message_stop = True\n elif evt_type == \"error\":\n err = evt.get(\"error\", {})\n emsg = err.get(\"message\", str(err)) if isinstance(err, dict) else str(err)\n warn = f\"\\n\\n!!!Error: SSE {emsg}\"; break\n if not warn:\n if not got_message_stop and not stop_reason: warn = \"\\n\\n[!!! 流异常中断,未收到完整响应 !!!]\"\n elif stop_reason == \"max_tokens\": warn = \"\\n\\n[!!! Response truncated: max_tokens !!!]\"\n if current_block:\n if current_block[\"type\"] == \"tool_use\":\n try: current_block[\"input\"] = json.loads(tool_json_buf) if tool_json_buf else {}\n except: current_block[\"input\"] = {\"_raw\": tool_json_buf}\n content_blocks.append(current_block); current_block = None\n if warn:\n print(f\"[WARN] {warn.strip()}\")\n insert_at = next((i for i,b in enumerate(content_blocks) if b.get(\"type\") == \"tool_use\"), len(content_blocks))\n content_blocks.insert(insert_at, {\"type\": \"text\", \"text\": warn}); yield warn\n return content_blocks\n\ndef _try_parse_tool_args(raw):\n \"\"\"Parse tool args string; split concatenated JSON objects like {..}{..} if needed.\n Returns list of parsed dicts.\"\"\"\n if not raw: return [{}]\n try: return [json.loads(raw)]\n except: pass\n parts = re.split(r'(?<=\\})(?=\\{)', raw)\n if len(parts) > 1:\n parsed = []\n for p in parts:\n try: parsed.append(json.loads(p))\n except: return [{\"_raw\": raw}]\n return parsed\n return [{\"_raw\": raw}]\n\ndef _parse_openai_sse(resp_lines, api_mode=\"chat_completions\"):\n \"\"\"Parse OpenAI SSE stream (chat_completions or responses API).\n Yields text chunks, returns list[content_block].\n content_block: {type:'text', text:str} | {type:'tool_use', id:str, name:str, input:dict}\n \"\"\"\n content_text = \"\"\n if api_mode == \"responses\":\n seen_delta = False; fc_buf = {}; current_fc_idx = None\n for line in resp_lines:\n if not line: continue\n line = line.decode('utf-8', errors='replace') if isinstance(line, bytes) else line\n if not line.startswith(\"data:\"): continue\n data_str = line[5:].lstrip()\n if data_str == \"[DONE]\": break\n try: evt = json.loads(data_str)\n except: continue\n etype = evt.get(\"type\", \"\")\n if etype == \"response.output_text.delta\":\n delta = evt.get(\"delta\", \"\")\n if delta: seen_delta = True; content_text += delta; yield delta\n elif etype == \"response.output_text.done\" and not seen_delta:\n text = evt.get(\"text\", \"\")\n if text: content_text += text; yield text\n elif etype == \"response.output_item.added\":\n item = evt.get(\"item\", {})\n if item.get(\"type\") == \"function_call\":\n idx = evt.get(\"output_index\", 0)\n fc_buf[idx] = {\"id\": item.get(\"call_id\", item.get(\"id\", \"\")), \"name\": item.get(\"name\", \"\"), \"args\": \"\"}\n current_fc_idx = idx\n elif etype == \"response.function_call_arguments.delta\":\n idx = evt.get(\"output_index\", current_fc_idx or 0)\n if idx in fc_buf: fc_buf[idx][\"args\"] += evt.get(\"delta\", \"\")\n elif etype == \"response.function_call_arguments.done\":\n idx = evt.get(\"output_index\", current_fc_idx or 0)\n if idx in fc_buf: fc_buf[idx][\"args\"] = evt.get(\"arguments\", fc_buf[idx][\"args\"])\n elif etype == \"error\":\n err = evt.get(\"error\", {})\n emsg = err.get(\"message\", str(err)) if isinstance(err, dict) else str(err)\n if emsg: content_text += f\"!!!Error: {emsg}\"; yield f\"!!!Error: {emsg}\"\n break\n elif etype == \"response.completed\":\n usage = evt.get(\"response\", {}).get(\"usage\", {})\n _record_usage(usage, api_mode)\n break\n blocks = []\n if content_text: blocks.append({\"type\": \"text\", \"text\": content_text})\n for idx in sorted(fc_buf):\n fc = fc_buf[idx]\n inps = _try_parse_tool_args(fc[\"args\"])\n for i, inp in enumerate(inps):\n bid = fc[\"id\"] or ''\n if len(inps) > 1: bid = f\"{bid}_{i}\" if bid else f\"split_{i}\"\n blocks.append({\"type\": \"tool_use\", \"id\": bid, \"name\": fc[\"name\"], \"input\": inp})\n return blocks\n else:\n tc_buf = {} # index -> {id, name, args}\n reasoning_text = \"\"\n for line in resp_lines:\n if not line: continue\n line = line.decode('utf-8', errors='replace') if isinstance(line, bytes) else line\n if not line.startswith(\"data:\"): continue\n data_str = line[5:].lstrip()\n if data_str == \"[DONE]\": break\n try: evt = json.loads(data_str)\n except: continue\n ch = (evt.get(\"choices\") or [{}])[0]\n delta = ch.get(\"delta\") or {}\n if delta.get(\"reasoning_content\"):\n reasoning_text += delta[\"reasoning_content\"]\n if delta.get(\"content\"):\n text = delta[\"content\"]; content_text += text; yield text\n for tc in (delta.get(\"tool_calls\") or []):\n idx = tc.get(\"index\", 0)\n has_name = bool(tc.get(\"function\", {}).get(\"name\"))\n if idx not in tc_buf:\n if has_name or not tc_buf: tc_buf[idx] = {\"id\": tc.get(\"id\") or '', \"name\": \"\", \"args\": \"\"}\n else: idx = max(tc_buf)\n if has_name: tc_buf[idx][\"name\"] = tc[\"function\"][\"name\"]\n if tc.get(\"function\", {}).get(\"arguments\"): tc_buf[idx][\"args\"] += tc[\"function\"][\"arguments\"]\n if tc.get(\"id\") and not tc_buf[idx][\"id\"]: tc_buf[idx][\"id\"] = tc[\"id\"]\n usage = evt.get(\"usage\")\n if usage: _record_usage(usage, api_mode)\n blocks = []\n if reasoning_text: blocks.append({\"type\": \"thinking\", \"thinking\": reasoning_text})\n if content_text: blocks.append({\"type\": \"text\", \"text\": content_text})\n for idx in sorted(tc_buf):\n tc = tc_buf[idx]\n inps = _try_parse_tool_args(tc[\"args\"])\n for i, inp in enumerate(inps):\n bid = tc[\"id\"] or ''\n if len(inps) > 1: bid = f\"{bid}_{i}\" if bid else f\"split_{i}\"\n blocks.append({\"type\": \"tool_use\", \"id\": bid, \"name\": tc[\"name\"], \"input\": inp})\n return blocks\n\ndef _record_usage(usage, api_mode):\n if not usage: return\n if api_mode == 'responses':\n cached = (usage.get(\"input_tokens_details\") or {}).get(\"cached_tokens\", 0)\n inp = usage.get(\"input_tokens\", 0); out = usage.get(\"output_tokens\", 0)\n print(f\"[Cache] input={inp} cached={cached}\")\n if out: print(f\"[Output] tokens={out}\")\n elif api_mode == 'chat_completions':\n cached = (usage.get(\"prompt_tokens_details\") or {}).get(\"cached_tokens\", 0)\n inp = usage.get(\"prompt_tokens\", 0); out = usage.get(\"completion_tokens\", 0)\n print(f\"[Cache] input={inp} cached={cached}\")\n if out: print(f\"[Output] tokens={out}\")\n elif api_mode == 'messages':\n ci, cr, inp = usage.get(\"cache_creation_input_tokens\", 0), usage.get(\"cache_read_input_tokens\", 0), usage.get(\"input_tokens\", 0)\n print(f\"[Cache] input={inp} creation={ci} read={cr}\")\n \ndef _parse_openai_json(data, api_mode=\"chat_completions\"):\n blocks = []\n if api_mode == \"responses\":\n _record_usage(data.get(\"usage\") or {}, api_mode)\n for item in (data.get(\"output\") or []):\n if item.get(\"type\") == \"message\":\n for p in (item.get(\"content\") or []):\n if p.get(\"type\") in (\"output_text\", \"text\") and p.get(\"text\"):\n blocks.append({\"type\": \"text\", \"text\": p[\"text\"]}); yield p[\"text\"]\n elif item.get(\"type\") == \"function_call\":\n try: args = json.loads(item.get(\"arguments\", \"\")) if item.get(\"arguments\") else {}\n except: args = {\"_raw\": item.get(\"arguments\", \"\")}\n blocks.append({\"type\": \"tool_use\", \"id\": item.get(\"call_id\", item.get(\"id\", \"\")),\n \"name\": item.get(\"name\", \"\"), \"input\": args})\n else:\n _record_usage(data.get(\"usage\") or {}, api_mode)\n msg = (data.get(\"choices\") or [{}])[0].get(\"message\", {})\n reasoning = msg.get(\"reasoning_content\", \"\")\n if reasoning:\n blocks.append({\"type\": \"thinking\", \"thinking\": reasoning})\n content = msg.get(\"content\", \"\")\n if content:\n blocks.append({\"type\": \"text\", \"text\": content}); yield content\n for tc in (msg.get(\"tool_calls\") or []):\n fn = tc.get(\"function\", {})\n try: args = json.loads(fn.get(\"arguments\", \"\")) if fn.get(\"arguments\") else {}\n except: args = {\"_raw\": fn.get(\"arguments\", \"\")}\n blocks.append({\"type\": \"tool_use\", \"id\": tc.get(\"id\", \"\"), \"name\": fn.get(\"name\", \"\"), \"input\": args})\n return blocks\n\ndef _stamp_oai_cache_markers(messages, model):\n \"\"\"Add cache_control to last 2 user messages for Anthropic models via OAI-compatible relay.\"\"\"\n ml = model.lower()\n if not any(k in ml for k in ('claude', 'anthropic')): return\n user_idxs = [i for i, m in enumerate(messages) if m.get('role') == 'user']\n for idx in user_idxs[-2:]:\n c = messages[idx].get('content')\n if isinstance(c, str):\n messages[idx] = {**messages[idx], 'content': [{'type': 'text', 'text': c, 'cache_control': {'type': 'ephemeral'}}]}\n elif isinstance(c, list) and c:\n c = list(c); c[-1] = dict(c[-1], cache_control={'type': 'ephemeral'})\n messages[idx] = {**messages[idx], 'content': c}\n\ndef _stream_with_retry(sess, url, headers, payload, parse_fn):\n _RETRYABLE = {408, 409, 425, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527, 529}\n def _delay(resp, attempt):\n try: ra = float((resp.headers or {}).get(\"retry-after\"))\n except: ra = None\n return max(0.5, ra if ra is not None else min(30.0, 1.5 * (2 ** attempt)))\n for attempt in range(sess.max_retries + 1):\n streamed = False\n try:\n with requests.post(url, headers=headers, json=payload, stream=sess.stream, \n timeout=(sess.connect_timeout, sess.read_timeout), proxies=sess.proxies, verify=sess.verify) as r:\n if r.status_code >= 400:\n if r.status_code in _RETRYABLE and attempt < sess.max_retries:\n d = _delay(r, attempt)\n print(f\"[LLM Retry] HTTP {r.status_code}, retry in {d:.1f}s ({attempt+1}/{sess.max_retries+1})\")\n time.sleep(d); continue\n try: body = r.text.strip()[:500]\n except: body = \"\"\n err = f\"!!!Error: HTTP {r.status_code}\" + (f\": {body}\" if body else \"\")\n yield err; return [{\"type\": \"text\", \"text\": err}]\n gen = parse_fn(r)\n try:\n while True: chunk = next(gen); streamed = True; yield chunk\n except StopIteration as e:\n if not e.value and not streamed: raise requests.ConnectionError(\"empty response\")\n return e.value or []\n except (requests.Timeout, requests.ConnectionError) as e:\n #pathlib.Path(__file__).parent.joinpath('temp','bad_requests.json').write_text(json.dumps({\"url\":url,\"headers\":headers,\"payload\":payload,\"err\":str(e),\"t\":time.time()},ensure_ascii=False),encoding='utf-8')\n err = f\"!!!Error: {type(e).__name__}: {e}\" if str(e) else f\"!!!Error: {type(e).__name__}\"\n if attempt < sess.max_retries:\n d = _delay(None, attempt)\n print(f\"[LLM Retry] {type(e).__name__}, retry in {d:.1f}s ({attempt+1}/{sess.max_retries+1})\")\n yield err; time.sleep(d); continue\n yield err; return [{\"type\": \"text\", \"text\": err}]\n except Exception as e:\n err = f\"\\n\\n[!!! 流异常中断 {type(e).__name__}: {e} !!!]\" if streamed else f\"!!!Error: {type(e).__name__}: {e}\"\n yield err; return [{\"type\": \"text\", \"text\": err}]\n\ndef _openai_stream(sess, messages):\n model, api_mode = sess.model, sess.api_mode\n ml = model.lower()\n temperature = sess.temperature\n if 'kimi' in ml or 'moonshot' in ml: temperature = 1\n elif 'minimax' in ml: temperature = max(0.01, min(temperature, 1.0)) # MiniMax requires temp in (0, 1]\n headers = {\"Authorization\": f\"Bearer {sess.api_key}\", \"Content-Type\": \"application/json\", \"Accept\": \"text/event-stream\", 'originator': 'codex_exec'}\n headers[\"User-Agent\"] = sess.user_agent\n if api_mode == \"responses\":\n url = auto_make_url(sess.api_base, \"responses\")\n payload = {\"model\": model, \"input\": _to_responses_input(messages), \"stream\": sess.stream, \n \"prompt_cache_key\": _RESP_CACHE_KEY, \"instructions\": sess.system or \"You are an Omnipotent Executor.\",\n \"client_metadata\": {\"x-codex-window-id\": f\"{_RESP_CACHE_KEY}:0\",\"x-codex-installation-id\": _RESP_CODEX_KEY},\n 'include': ['reasoning.encrypted_content']}\n if sess.reasoning_effort: payload[\"reasoning\"] = {\"effort\": sess.reasoning_effort}\n if sess.max_tokens: payload[\"max_output_tokens\"] = sess.max_tokens\n else:\n url = auto_make_url(sess.api_base, \"chat/completions\")\n if sess.system: messages = [{\"role\": \"system\", \"content\": sess.system}] + messages\n _stamp_oai_cache_markers(messages, model)\n payload = {\"model\": model, \"messages\": messages, \"stream\": sess.stream}\n if sess.stream: payload[\"stream_options\"] = {\"include_usage\": True}\n if temperature != 1: payload[\"temperature\"] = temperature\n if sess.max_tokens: payload[\"max_completion_tokens\" if ml.startswith((\"gpt-5\", \"o1\", \"o2\", \"o3\", \"o4\")) else \"max_tokens\"] = sess.max_tokens\n if sess.reasoning_effort: payload[\"reasoning_effort\"] = sess.reasoning_effort\n tools = getattr(sess, 'tools', None)\n if tools: payload[\"tools\"] = _prepare_oai_tools(tools, api_mode)\n if sess.service_tier: payload[\"service_tier\"] = sess.service_tier\n parse_fn = (lambda r: _parse_openai_sse(r.iter_lines(), api_mode)) if sess.stream else (lambda r: _parse_openai_json(r.json(), api_mode))\n return (yield from _stream_with_retry(sess, url, headers, payload, parse_fn))\n \ndef _prepare_oai_tools(tools, api_mode=\"chat_completions\"):\n if api_mode == \"responses\":\n resp_tools = []\n for t in tools:\n if t.get(\"type\") == \"function\" and \"function\" in t:\n rt = {\"type\": \"function\"}; rt.update(t[\"function\"])\n resp_tools.append(rt)\n else: resp_tools.append(t)\n return resp_tools\n return tools\n\ndef _to_responses_input(messages):\n result, pending = [], []\n for msg in messages:\n role = str(msg.get(\"role\", \"user\")).lower()\n if role == \"tool\":\n cid = msg.get(\"tool_call_id\") or (pending.pop(0) if pending else f\"call_{uuid.uuid4().hex[:8]}\")\n result.append({\"type\": \"function_call_output\", \"call_id\": cid, \"output\": msg.get(\"content\", \"\")})\n continue\n if role not in [\"user\", \"assistant\", \"system\", \"developer\"]: role = \"user\"\n if role == \"system\": role = \"developer\" # Responses API uses 'developer' instead of 'system'\n content = msg.get(\"content\", \"\")\n text_type = \"output_text\" if role == \"assistant\" else \"input_text\"\n parts = []\n if isinstance(content, str):\n if content: parts.append({\"type\": text_type, \"text\": content})\n elif isinstance(content, list):\n for part in content:\n if not isinstance(part, dict): continue\n ptype = part.get(\"type\")\n if ptype == \"text\":\n text = part.get(\"text\", \"\")\n if text: parts.append({\"type\": text_type, \"text\": text})\n elif ptype == \"image_url\":\n url = (part.get(\"image_url\") or {}).get(\"url\", \"\")\n if url and role != \"assistant\": parts.append({\"type\": \"input_image\", \"image_url\": url})\n if len(parts) == 0: parts = [{\"type\": text_type, \"text\": str(content) if not isinstance(content, list) else '[empty]'}]\n result.append({\"role\": role, \"content\": parts})\n pending = []\n for tc in (msg.get(\"tool_calls\") or []):\n f = tc.get(\"function\", {})\n cid = tc.get(\"id\") or f\"call_{uuid.uuid4().hex[:8]}\"\n pending.append(cid)\n result.append({\"type\": \"function_call\", \"call_id\": cid, \"name\": f.get(\"name\", \"\"), \"arguments\": f.get(\"arguments\", \"\")})\n return result\n\n\ndef _msgs_claude2oai(messages):\n result = []\n for msg in messages:\n role = msg.get(\"role\", \"user\")\n content = msg.get(\"content\", \"\")\n blocks = content if isinstance(content, list) else [{\"type\": \"text\", \"text\": str(content)}]\n if role == \"assistant\":\n text_parts, tool_calls, reasoning = [], [], \"\"\n for b in blocks:\n if not isinstance(b, dict): continue\n if b.get(\"type\") == \"thinking\" and b.get(\"thinking\"): reasoning = b[\"thinking\"]\n elif b.get(\"type\") == \"text\" and b.get(\"text\"): text_parts.append({\"type\": \"text\", \"text\": b.get(\"text\", \"\")})\n elif b.get(\"type\") == \"tool_use\":\n tool_calls.append({\n \"id\": b.get(\"id\") or '', \"type\": \"function\",\n \"function\": {\"name\": b.get(\"name\", \"\"), \"arguments\": json.dumps(b.get(\"input\", {}), ensure_ascii=False)}\n })\n m = {\"role\": \"assistant\"}\n if reasoning: m[\"reasoning_content\"] = reasoning\n if text_parts: m[\"content\"] = text_parts\n elif not tool_calls: m[\"content\"] = \".\"\n if tool_calls: m[\"tool_calls\"] = tool_calls\n result.append(m)\n elif role == \"user\":\n text_parts = []\n for b in blocks:\n if not isinstance(b, dict): continue\n if b.get(\"type\") == \"tool_result\":\n if text_parts:\n result.append({\"role\": \"user\", \"content\": text_parts})\n text_parts = []\n tr = b.get(\"content\", \"\")\n if isinstance(tr, list):\n tr = \"\\n\".join(x.get(\"text\", \"\") for x in tr if isinstance(x, dict) and x.get(\"type\") == \"text\")\n result.append({\"role\": \"tool\", \"tool_call_id\": b.get(\"tool_use_id\") or '', \"content\": tr if isinstance(tr, str) else str(tr)})\n elif b.get(\"type\") == \"image\":\n src = b.get(\"source\") or {}\n if src.get(\"type\") == \"base64\" and src.get(\"data\"):\n text_parts.append({\"type\": \"image_url\", \"image_url\": {\"url\": f\"data:{src.get('media_type', 'image/png')};base64,{src.get('data', '')}\"}})\n elif b.get(\"type\") == \"image_url\": text_parts.append(b)\n elif b.get(\"type\") == \"text\" and b.get(\"text\"): text_parts.append({\"type\": \"text\", \"text\": b.get(\"text\", \"\")})\n if text_parts: result.append({\"role\": \"user\", \"content\": text_parts})\n else: result.append(msg)\n return result\n\n\nclass BaseSession:\n def __init__(self, cfg):\n self.api_key = cfg['apikey']\n self.api_base = cfg['apibase'].rstrip('/')\n self.model = cfg.get('model', '')\n default_context_win = 30000\n if 'deepseek' in self.model.lower():\n default_context_win = 70000; self.cut_msg_interval = 25; self.trim_keep_rate = 0.3\n self.context_win = cfg.get('context_win', default_context_win)\n self.history = []; self.lock = threading.Lock(); self.system = \"\"\n self.name = cfg.get('name', self.model)\n proxy = cfg.get('proxy'); \n self.proxies = {\"http\": proxy, \"https\": proxy} if proxy else None\n self.max_retries = max(0, int(cfg.get('max_retries', 4)))\n self.verify = cfg.get('verify', True)\n self.stream = cfg.get('stream', True)\n default_ct, default_rt = (5, 40) if self.stream else (10, 240)\n self.connect_timeout = max(1, int(cfg.get('timeout', default_ct)))\n self.read_timeout = max(5, int(cfg.get('read_timeout', default_rt)))\n def _enum(key, valid):\n v = cfg.get(key); v = None if v is None else str(v).strip().lower()\n return v if not v or v in valid else print(f\"[WARN] Invalid {key} {v!r}, ignored.\")\n self.reasoning_effort = _enum('reasoning_effort', {'none', 'minimal', 'low', 'medium', 'high', 'xhigh'})\n self.service_tier = _enum('service_tier', {'auto', 'default', 'priority', 'flex'})\n self.thinking_type = _enum('thinking_type', {'adaptive', 'enabled', 'disabled'})\n self.thinking_budget_tokens = cfg.get('thinking_budget_tokens')\n mode = str(cfg.get('api_mode', 'chat_completions')).strip().lower().replace('-', '_')\n self.api_mode = 'responses' if mode in ('responses', 'response') else 'chat_completions'\n self.temperature = cfg.get('temperature', 1)\n self.max_tokens = cfg.get('max_tokens')\n self.default_ua = \"claude-cli/2.1.152 (external, cli)\"\n self.user_agent = cfg.get(\"user_agent\", self.default_ua)\n def _apply_claude_thinking(self, payload):\n if self.thinking_type:\n thinking = {\"type\": self.thinking_type}\n if self.thinking_type == 'enabled':\n if self.thinking_budget_tokens is None: print(\"[WARN] thinking_type='enabled' requires thinking_budget_tokens, ignored.\")\n else:\n thinking[\"budget_tokens\"] = self.thinking_budget_tokens; payload[\"thinking\"] = thinking\n else: payload[\"thinking\"] = thinking\n if self.reasoning_effort:\n effort = {'low': 'low', 'medium': 'medium', 'high': 'high', 'xhigh': 'max'}.get(self.reasoning_effort)\n if effort: payload[\"output_config\"] = {\"effort\": effort}\n else: print(f\"[WARN] reasoning_effort {self.reasoning_effort!r} is unsupported for Claude output_config.effort, ignored.\")\n def ask(self, prompt):\n def _ask_gen():\n with self.lock:\n self.history.append({\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": prompt}]})\n trim_messages_history(self.history, self)\n messages = self.make_messages(self.history)\n content_blocks = None; content = ''\n gen = self.raw_ask(messages)\n try:\n while True: chunk = next(gen); content += chunk; yield chunk\n except StopIteration as e: content_blocks = e.value or []\n if len(content_blocks) > 1: print(f\"[DEBUG BaseSession.ask] content_blocks: {content_blocks}\")\n for block in (content_blocks or []):\n if block.get('type', '') == 'tool_use':\n tu = {'name': block.get('name', ''), 'arguments': block.get('input', {})}\n yield f'<tool_use>{json.dumps(tu, ensure_ascii=False)}</tool_use>'\n if content.strip() and not content.startswith(\"!!!Error:\"): self.history.append({\"role\": \"assistant\", \"content\": [{\"type\": \"text\", \"text\": content}]})\n return _ask_gen()\n\ndef _keep_claude_block(b): return not isinstance(b, dict) or b.get(\"type\") != \"thinking\" or b.get(\"signature\")\ndef _drop_unsigned_thinking(messages):\n for m in messages:\n c = m.get(\"content\")\n if isinstance(c, list): m[\"content\"] = [b for b in c if _keep_claude_block(b)]\n return messages\n\ndef _ensure_thinking_blocks(messages, model):\n \"\"\"deepseek needs thinking in history!\"\"\"\n if 'deepseek' not in model.lower(): return messages\n for m in messages:\n if m.get(\"role\") != \"assistant\": continue\n c = m.get(\"content\")\n if not isinstance(c, list): continue\n has_thinking = any(isinstance(b, dict) and b.get(\"type\") == \"thinking\" for b in c)\n if not has_thinking: m[\"content\"] = [{\"type\": \"thinking\", \"thinking\": \"...\", \"signature\": \"placeholder\"}, *c]\n return messages\n\nclass ClaudeSession(BaseSession):\n def raw_ask(self, messages):\n messages = _fix_messages(messages)\n if self.max_tokens is None: self.max_tokens = 8192\n headers = {\"x-api-key\": self.api_key, \"Content-Type\": \"application/json\", \"anthropic-version\": \"2023-06-01\", \"anthropic-beta\": \"prompt-caching-2024-07-31\"}\n payload = {\"model\": self.model, \"messages\": messages, \"max_tokens\": self.max_tokens, \"stream\": self.stream}\n if self.temperature != 1: payload[\"temperature\"] = self.temperature\n self._apply_claude_thinking(payload)\n if self.system: payload[\"system\"] = [{\"type\": \"text\", \"text\": self.system, \"cache_control\": {\"type\": \"persistent\"}}]\n url = auto_make_url(self.api_base, \"messages\")\n parse_fn = (lambda r: _parse_claude_sse(r.iter_lines())) if self.stream else (lambda r: _parse_claude_json(r.json()))\n return (yield from _stream_with_retry(self, url, headers, payload, parse_fn))\n def make_messages(self, raw_list):\n msgs = _drop_unsigned_thinking([{\"role\": m['role'], \"content\": list(m['content'])} for m in raw_list])\n user_idxs = [i for i, m in enumerate(msgs) if m['role'] == 'user']\n for idx in user_idxs[-2:]:\n msgs[idx][\"content\"][-1] = dict(msgs[idx][\"content\"][-1], cache_control={\"type\": \"ephemeral\"})\n return msgs\n\nclass LLMSession(BaseSession):\n def raw_ask(self, messages): return (yield from _openai_stream(self, messages))\n def make_messages(self, raw_list): return _msgs_claude2oai(_fix_messages(raw_list))\n\ndef _fix_messages(messages):\n if not messages: return messages\n W = lambda c: c if isinstance(c, list) else [{\"type\": \"text\", \"text\": str(c)}]\n merged = []\n for m in messages:\n if m.get('role') not in ('user', 'assistant'): continue\n blocks = W(m.get('content', []))\n if merged and m['role'] == merged[-1]['role']:\n merged[-1]['content'] += [{\"type\": \"text\", \"text\": \"\\n\"}] + blocks\n else:\n merged.append({\"role\": m['role'], \"content\": list(blocks)})\n while merged and merged[0]['role'] != 'user': merged.pop(0)\n if not merged: return []\n prev_uses = []\n for m in merged:\n c = m['content']\n if m['role'] == 'assistant':\n seen, out = set(), []\n for b in c:\n uid = b.get('id') if isinstance(b, dict) and b.get('type') == 'tool_use' else None\n if uid and uid in seen: continue\n if uid: seen.add(uid)\n out.append(b)\n m['content'] = out\n prev_uses = [b.get('id') for b in out if isinstance(b, dict) and b.get('type') == 'tool_use']\n else:\n got, rest = {}, []\n for b in c:\n tid = b.get('tool_use_id') if isinstance(b, dict) and b.get('type') == 'tool_result' else None\n if tid and tid in prev_uses and tid not in got: got[tid] = b\n elif isinstance(b, dict) and b.get('type') == 'tool_result': rest.append({\"type\": \"text\", \"text\": str(b.get('content', ''))})\n else: rest.append(b)\n m['content'] = [got.get(u) or {\"type\": \"tool_result\", \"tool_use_id\": u, \"content\": \"(error)\"} for u in prev_uses] + rest\n prev_uses = []\n return merged\n\nclass NativeClaudeSession(BaseSession):\n native_ua = \"claude-cli/2.1.152 (native, cli)\"\n def __init__(self, cfg):\n super().__init__(cfg)\n self.fake_cc_system_prompt = cfg.get(\"fake_cc_system_prompt\", False)\n self._session_id = str(uuid.uuid4())\n self._account_uuid = str(uuid.uuid4())\n self._device_id = uuid.uuid4().hex + uuid.uuid4().hex[:32]\n self.tools = None\n if self.user_agent == self.default_ua: self.user_agent = self.native_ua\n def raw_ask(self, messages):\n if self.max_tokens is None: self.max_tokens = 8192\n model = self.model\n messages = _fix_messages(messages)\n if 'claude' in model.lower(): messages = _drop_unsigned_thinking(messages)\n messages = _ensure_thinking_blocks(messages, self.model)\n beta_parts = [\"claude-code-20250219\", \"interleaved-thinking-2025-05-14\", \"redact-thinking-2026-02-12\", \"context-management-2025-06-27\", \"prompt-caching-scope-2026-01-05\", \"effort-2025-11-24\"]\n if \"[1m]\" in model.lower():\n beta_parts.insert(1, \"context-1m-2025-08-07\"); model = model.replace(\"[1m]\", \"\").replace(\"[1M]\", \"\")\n headers = {\"Content-Type\": \"application/json\", \"anthropic-version\": \"2023-06-01\",\n \"anthropic-beta\": \",\".join(beta_parts), \"anthropic-dangerous-direct-browser-access\": \"true\",\n \"user-agent\": self.user_agent, \"x-app\": \"cli\"}\n headers.update({\"Accept\": \"application/json\", \"X-Claude-Code-Session-Id\": self._session_id, \"X-Stainless-Arch\": \"x64\", \"X-Stainless-Lang\": \"js\", \"X-Stainless-OS\": \"Windows\", \"X-Stainless-Package-Version\": \"0.94.0\", \"X-Stainless-Retry-Count\": \"0\", \"X-Stainless-Runtime\": \"node\", \"X-Stainless-Runtime-Version\": \"v24.3.0\", \"X-Stainless-Timeout\": \"600\"})\n if self.api_key.startswith(\"sk-ant-\"): headers[\"x-api-key\"] = self.api_key\n else: headers[\"authorization\"] = f\"Bearer {self.api_key}\"\n payload = {\"model\": model, \"messages\": messages, \"max_tokens\": self.max_tokens, \"stream\": self.stream}\n #if self.fake_cc_system_prompt: payload[\"max_tokens\"] = 64000\n if self.temperature != 1: payload[\"temperature\"] = self.temperature\n self._apply_claude_thinking(payload)\n payload[\"context_management\"] = {\"edits\": [{\"type\": \"clear_thinking_20251015\", \"keep\": \"all\"}]}; \n if self.fake_cc_system_prompt:\n if 'thinking' not in payload: payload[\"thinking\"] = {\"type\": \"adaptive\"}\n if 'output_config' not in payload: payload[\"output_config\"] = {\"effort\": \"medium\"}\n payload[\"metadata\"] = {\"user_id\": json.dumps({\"device_id\": self._device_id, \"account_uuid\": \"\", \"session_id\": self._session_id}, separators=(',', ':'))}\n if self.tools:\n claude_tools = openai_tools_to_claude(self.tools)\n tools = [dict(t) for t in claude_tools]; tools[-1][\"cache_control\"] = {\"type\": \"ephemeral\"}\n payload[\"tools\"] = tools\n else: print(\"[ERROR] No tools provided for this session.\")\n payload['system'] = [{\"type\": \"text\", \"text\": \"You are Claude Code, Anthropic's official CLI for Claude.\", \"cache_control\": {\"type\": \"ephemeral\"}}]\n if self.system:\n if self.fake_cc_system_prompt: payload[\"system\"].append({\"type\": \"text\", \"text\": self.system})\n else: payload[\"system\"] = [{\"type\": \"text\", \"text\": self.system}]\n user_idxs = [i for i, m in enumerate(messages) if m['role'] == 'user']\n for idx in user_idxs[-2:]:\n messages[idx] = {**messages[idx], \"content\": list(messages[idx][\"content\"])}\n messages[idx][\"content\"][-1] = dict(messages[idx][\"content\"][-1], cache_control={\"type\": \"ephemeral\"})\n url = auto_make_url(self.api_base, \"messages\") + '?beta=true'\n parse_fn = (lambda r: _parse_claude_sse(r.iter_lines())) if self.stream else (lambda r: _parse_claude_json(r.json()))\n return (yield from _stream_with_retry(self, url, headers, payload, parse_fn))\n\n def ask(self, msg):\n assert type(msg) is dict\n with self.lock:\n self.history.append(msg)\n trim_messages_history(self.history, self)\n messages = [{\"role\": m[\"role\"], \"content\": list(m[\"content\"])} for m in self.history]\n content_blocks = None\n gen = self.raw_ask(messages)\n try:\n while True: yield next(gen)\n except StopIteration as e: content_blocks = e.value or []\n if content_blocks and (_injected := _ensure_text_block(content_blocks)): yield _injected\n if content_blocks and not (len(content_blocks) == 1 and content_blocks[0].get(\"text\", \"\").startswith(\"!!!Error:\")):\n self.history.append({\"role\": \"assistant\", \"content\": content_blocks})\n text_parts = [b[\"text\"] for b in content_blocks if b.get(\"type\") == \"text\"]\n content = \"\\n\".join(text_parts).strip()\n tool_calls = [MockToolCall(b[\"name\"], b.get(\"input\", {}), id=b.get(\"id\", \"\")) for b in content_blocks if b.get(\"type\") == \"tool_use\"]\n if not tool_calls: tool_calls, content = _parse_text_tool_calls(content)\n thinking_parts = [b[\"thinking\"] for b in content_blocks if b.get(\"type\") == \"thinking\"]\n thinking = \"\\n\".join(thinking_parts).strip()\n if not thinking:\n think_pattern = r\"<think(?:ing)?>(.*?)</think(?:ing)?>\"\n think_match = re.search(think_pattern, content, re.DOTALL)\n if think_match:\n thinking = think_match.group(1).strip()\n content = re.sub(think_pattern, \"\", content, flags=re.DOTALL)\n return MockResponse(thinking, content, tool_calls, str(content_blocks))\n\nclass NativeOAISession(NativeClaudeSession):\n native_ua = \"codex_exec/0.139.0 (Windows 10.0.26200; x86_64) unknown (codex_exec; 0.139.0)\"\n def raw_ask(self, messages):\n messages = _fix_messages(messages)\n messages = _ensure_thinking_blocks(messages, self.model)\n return (yield from _openai_stream(self, _msgs_claude2oai(messages)))\n\ndef openai_tools_to_claude(tools):\n \"\"\"[{type:'function', function:{name,description,parameters}}] → [{name,description,input_schema}].\"\"\"\n result = []\n for t in tools:\n if 'input_schema' in t: result.append(t); continue # 已是claude格式\n fn = t.get('function', t)\n result.append({'name': fn['name'], 'description': fn.get('description', ''),\n 'input_schema': fn.get('parameters', {'type': 'object', 'properties': {}})})\n return result\n\nclass MockFunction:\n def __init__(self, name, arguments): self.name, self.arguments = name, arguments \n \nclass MockToolCall:\n def __init__(self, name, args, id=''):\n arg_str = json.dumps(args, ensure_ascii=False) if isinstance(args, (dict, list)) else (args or '{}')\n self.function = MockFunction(name, arg_str); self.id = id\n\nclass MockResponse:\n def __init__(self, thinking, content, tool_calls, raw, stop_reason='end_turn'):\n self.thinking = thinking; self.content = content \n self.tool_calls = tool_calls; self.raw = raw\n self.stop_reason = 'tool_use' if tool_calls else stop_reason\n def __repr__(self): \n return f\"<MockResponse thinking={bool(self.thinking)}, content='{self.content}', tools={bool(self.tool_calls)}>\"\n\nclass ToolClient:\n def __init__(self, backend, auto_save_tokens=True):\n self.backend = backend\n self.auto_save_tokens = auto_save_tokens\n self.last_tools = ''\n self.name = self.backend.name\n self.total_cd_tokens = 0\n self.log_path = None\n\n def chat(self, messages, tools=None):\n tools = json.loads(json.dumps(tools, ensure_ascii=False)) if tools else tools\n for t in tools or []:\n f = t.get('function', {})\n if f.get('name') == 'file_write':\n props = f.get('parameters', {}).get('properties', {})\n props.pop('content', None)\n extra = '. Content must be placed in <file_content> tags in reply body, not in args'\n if extra not in f.get('description', ''): f['description'] = f.get('description', '') + extra\n break\n full_prompt = self._build_protocol_prompt(messages, tools)\n print(\"Full prompt length:\", len(full_prompt), 'chars')\n gen = self.backend.ask(full_prompt)\n _write_llm_log('Prompt', full_prompt, self.log_path)\n raw_text = ''\n for chunk in gen:\n raw_text += chunk; yield chunk\n _write_llm_log('Response', raw_text, self.log_path)\n return self._parse_mixed_response(raw_text)\n\n def _prepare_tool_instruction(self, tools):\n tool_instruction = \"\"\n if not tools: return tool_instruction\n tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':'))\n _en = os.environ.get('GA_LANG') == 'en'\n if _en:\n tool_instruction = f\"\"\"\n### Interaction Protocol (must follow strictly, always in effect)\nFollow these steps to think and act:\n1. **Think**: Analyze the current situation and strategy inside `<thinking>` tags.\n2. **Summarize**: Output a minimal one-line (<30 words) physical snapshot in `<summary>`: new info from last tool result + current tool call intent. This goes into long-term working memory. Must contain real information, no filler.\n3. **Act**: If you need to call tools, output one or more **<tool_use> blocks** after your reply, then stop.\n\"\"\"\n else:\n tool_instruction = f\"\"\"\n### 交互协议 (必须严格遵守,持续有效)\n请按照以下步骤思考并行动:\n1. **思考**: 在 `<thinking>` 标签中先进行思考,分析现状和策略。\n2. **总结**: 在 `<summary>` 中输出*极为简短*的高度概括的单行(<30字)物理快照,包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。\n3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**<tool_use>块**,然后结束。\n\"\"\"\n tool_instruction += f'\\nFormat: ```<tool_use>{{\"name\": \"tool_name\", \"arguments\": {{...}}}}</tool_use>```\\n\\n### Tools (mounted, always in effect):\\n{tools_json}\\n'\n if self.auto_save_tokens and self.last_tools == tools_json:\n tool_instruction = \"\\n### Tools: still active, **ready to call**. Protocol unchanged.\\n\" if _en else \"\\n### 工具库状态:持续有效(code_run/file_read等),**可正常调用**。调用协议沿用。\\n\"\n else: self.total_cd_tokens = 0\n self.last_tools = tools_json\n return tool_instruction\n\n def _build_protocol_prompt(self, messages, tools):\n system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), \"\")\n history_msgs = [m for m in messages if m['role'].lower() != 'system']\n tool_instruction = self._prepare_tool_instruction(tools)\n system = \"\"; user = \"\"\n if system_content: system += f\"{system_content}\\n\"\n system += f\"{tool_instruction}\"\n for m in history_msgs:\n role = \"USER\" if m['role'] == 'user' else \"ASSISTANT\"\n user += f\"=== {role} ===\\n\"\n for tr in m.get('tool_results', []): user += f'<tool_result>{tr[\"content\"]}</tool_result>\\n'\n user += str(m['content']) + \"\\n\"\n self.total_cd_tokens += len(user) // 3\n if self.total_cd_tokens > 9000: self.last_tools = ''\n user += \"=== ASSISTANT ===\\n\" \n return system + user\n\n def _parse_mixed_response(self, text):\n remaining_text = text; thinking = ''\n think_match = re.search(r\"<think(?:ing)?>(.*?)</think(?:ing)?>\", text, re.DOTALL)\n if think_match:\n thinking = think_match.group(1).strip()\n remaining_text = re.sub(r\"<think(?:ing)?>(.*?)</think(?:ing)?>\", \"\", remaining_text, flags=re.DOTALL)\n tool_calls, remaining_text = _parse_text_tool_calls(remaining_text)\n if not tool_calls:\n json_strs = []; errors = []\n if '<tool_use>' in remaining_text:\n weaktoolstr = remaining_text.split('<tool_use>')[-1].strip().strip('><')\n json_str = weaktoolstr if weaktoolstr.endswith('}') else ''\n if json_str == '' and '```' in weaktoolstr and weaktoolstr.split('```')[0].strip().endswith('}'):\n json_str = weaktoolstr.split('```')[0].strip()\n if json_str: json_strs.append(json_str)\n remaining_text = remaining_text.replace('<tool_use>'+weaktoolstr, \"\")\n elif '\"name\":' in remaining_text and '\"arguments\":' in remaining_text:\n json_match = re.search(r'\\{.*\"name\":.*\\}', remaining_text, re.DOTALL)\n if json_match:\n json_strs.append(json_match.group(0).strip())\n remaining_text = remaining_text.replace(json_match.group(0), \"\").strip()\n for json_str in json_strs:\n try:\n data = tryparse(json_str)\n func_name = data.get('name') or data.get('function') or data.get('tool')\n args = data.get('arguments') or data.get('args') or data.get('params') or data.get('parameters')\n if args is None: args = data\n if func_name: tool_calls.append(MockToolCall(func_name, args))\n except json.JSONDecodeError:\n errors.append(f'Failed to parse tool_use JSON: {json_str[:200]}')\n self.last_tools = ''\n except: pass\n if not tool_calls:\n for e in errors:\n print(f\"[Warn] {e}\"); tool_calls.append(MockToolCall('bad_json', {'msg': e}))\n return MockResponse(thinking, remaining_text.strip(), tool_calls, text)\n\ndef _parse_text_tool_calls(content):\n \"\"\"Fallback: extract tool calls from text when model doesn't use native tool_use blocks.\"\"\"\n tcs = []\n # try JSON array: [{\"type\":\"tool_use\", \"name\":..., \"input\":...}]\n _jp = next((p for p in ['[{\"type\":\"tool_use\"', '[{\"type\": \"tool_use\"'] if p in content), None)\n if _jp and content.endswith('}]'):\n try:\n idx = content.index(_jp); raw = json.loads(content[idx:])\n tcs = [MockToolCall(b[\"name\"], b.get(\"input\", {}), id=b.get(\"id\", \"\")) for b in raw if b.get(\"type\") == \"tool_use\"]\n return tcs, content[:idx].strip()\n except: pass\n # try XML tags: <tool_call>{\"name\":..., \"arguments\":...}</tool_call>\n _xp = r\"<(?:tool_use|tool_call)>((?:(?!<(?:tool_use|tool_call)>).){15,}?)</(?:tool_use|tool_call)>\"\n for s in re.findall(_xp, content, re.DOTALL):\n try:\n d = tryparse(s.strip()); name = d.get('name')\n args = d.get('arguments') or d.get('args') or d.get('input') or {}\n if name: tcs.append(MockToolCall(name, args))\n except: pass\n if tcs: content = re.sub(_xp, \"\", content, flags=re.DOTALL).strip()\n return tcs, content\n\ndef _ensure_text_block(blocks):\n \"\"\"If response has thinking but no text block, inject a synthetic summary from thinking's first line.\"\"\"\n if any(b.get(\"type\") == \"text\" for b in blocks): return None\n th = next((b.get(\"thinking\", \"\") for b in blocks if b.get(\"type\") == \"thinking\"), \"\")\n if not th: return None\n line = th.strip().split('\\n', 1)[0]\n txt = \"<summary>\" + (line[:60] + '...' if len(line) > 60 else line) + \"</summary>\"\n blocks.insert(1, {\"type\": \"text\", \"text\": txt})\n return txt\n\ndef _write_llm_log(label, content, log_path=None):\n if not log_path:\n log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), f'temp/model_responses/model_responses_{os.getpid()}.txt')\n os.makedirs(os.path.dirname(os.path.abspath(log_path)), exist_ok=True)\n ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\n with open(log_path, 'a', encoding='utf-8', errors='replace') as f:\n f.write(f\"=== {label} === {ts}\\n{content}\\n\\n\")\n\ndef tryparse(json_str):\n try: return json.loads(json_str)\n except: pass\n json_str = json_str.strip().strip('`').replace('json\\n', '', 1).strip()\n try: return json.loads(json_str)\n except: pass\n try: return json.loads(json_str[:-1])\n except: pass\n if '}' in json_str: json_str = json_str[:json_str.rfind('}') + 1]\n return json.loads(json_str)\n\nclass MixinSession:\n \"\"\"Multi-session fallback with spring-back to primary.\"\"\"\n def __init__(self, all_sessions, cfg):\n self._retries, self._base_delay = cfg.get('max_retries', 3), cfg.get('base_delay', 1.5)\n self._spring_sec = cfg.get('spring_back', 300)\n self._sessions = [all_sessions[i].backend if isinstance(i, int) else \n next(s.backend for s in all_sessions if type(s) is not dict and s.backend.name == i) for i in cfg.get('llm_nos', [])]\n is_native = lambda s: 'Native' in s.__class__.__name__\n groups = {is_native(s) for s in self._sessions}\n assert len(groups) == 1, f\"MixinSession: sessions must be in same group (Native or non-Native), got {[type(s).__name__ for s in self._sessions]}\"\n self.name = '|'.join(s.name for s in self._sessions)\n import copy; self._sessions = [copy.copy(s) for s in self._sessions]\n for s in self._sessions: s.max_retries = 0\n self._orig_raw_asks = [s.raw_ask for s in self._sessions]\n self._sessions[0].raw_ask = self._raw_ask\n self._cur_idx, self._switched_at = 0, 0.0\n def __getattr__(self, name): return getattr(self._sessions[0], name)\n _BROADCAST_ATTRS = frozenset({'system', 'tools', 'temperature', 'max_tokens', 'reasoning_effort', 'history', 'stream', 'read_timeout'})\n def __setattr__(self, name, value):\n if name in self._BROADCAST_ATTRS:\n for s in self._sessions:\n v = openai_tools_to_claude(value) if name == 'tools' and type(s) is NativeClaudeSession else value\n setattr(s, name, v)\n else: object.__setattr__(self, name, value)\n @property\n def primary(self): return self._sessions[0]\n @property\n def model(self): return getattr(self._sessions[self._cur_idx], 'model', None)\n def _pick(self):\n if self._cur_idx and time.time() - self._switched_at > self._spring_sec: self._cur_idx = 0\n return self._cur_idx\n def _raw_ask(self, *args, **kwargs):\n base, n = self._pick(), len(self._sessions)\n test_error = lambda x: isinstance(x, str) and x.lstrip().startswith(('!!!Error:', '[Error:'))\n for attempt in range(self._retries + 1):\n idx = (base + attempt) % n\n gen = self._orig_raw_asks[idx](*args, **kwargs)\n print(f'[MixinSession] Using session ({self._sessions[idx].name})')\n last_chunk, return_val, yielded = None, [], False\n try:\n while True:\n chunk = next(gen); last_chunk = chunk\n if not yielded and test_error(chunk): continue\n yield chunk; yielded = True\n except StopIteration as e: return_val = e.value or []\n is_err = test_error(last_chunk)\n if not is_err:\n if attempt > 0: self._cur_idx = idx; self._switched_at = time.time()\n elif isinstance(last_chunk, str) and '[!!! 流异常中断' in last_chunk and n > 1:\n self._cur_idx = (idx + 1) % n; self._switched_at = time.time()\n print(f'[MixinSession] Partial failure, next call → s{self._cur_idx} ({self._sessions[self._cur_idx].name})')\n return return_val\n if attempt >= self._retries:\n yield last_chunk; return return_val\n nxt = (base + attempt + 1) % n\n if nxt == base: # full round failed, delay before next\n rnd = (attempt + 1) // n\n delay = min(30, self._base_delay * (1.5 ** rnd))\n print(f'[MixinSession] {last_chunk[:80]}, round {rnd} exhausted, retry in {delay:.1f}s')\n time.sleep(delay)\n else: print(f'[MixinSession] {last_chunk[:80]}, retry {attempt+1}/{self._retries} (s{idx}→s{nxt})')\n\nTHINKING_PROMPT_ZH = \"\"\"\n### 行动规范(持续有效)\n每次回复(含工具调用轮)都先在回复文字中包含一个<summary></summary> 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。\n\\n**若用户需求未完成,必须进行工具调用!**\n\"\"\".strip()\nTHINKING_PROMPT_EN = \"\"\"\n### Action Protocol (always in effect)\nThe reply body should first include a minimal one-line (<30 words) physical snapshot in <summary></summary>: new info from last result + current intent. This goes into long-term working memory.\n\\n**If the user's request is not yet complete, tool calls are required!**\n\"\"\".strip()\n\nclass NativeToolClient:\n @staticmethod\n def _thinking_prompt(): return THINKING_PROMPT_EN if os.environ.get('GA_LANG') == 'en' else THINKING_PROMPT_ZH\n def __init__(self, backend):\n self.backend = backend\n self.backend.system = self._thinking_prompt()\n self.name = self.backend.name\n self._pending_tool_ids = []\n self.log_path = None\n def set_system(self, extra_system):\n combined = f\"{extra_system}\\n\\n{self._thinking_prompt()}\" if extra_system else self._thinking_prompt()\n if combined != self.backend.system: print(f\"[Debug] Updated system prompt, length {len(combined)} chars.\")\n self.backend.system = combined\n def chat(self, messages, tools=None):\n if tools: self.backend.tools = tools\n if not self.backend.history: self._pending_tool_ids = []\n combined_content = []; resp = None; tool_results = []\n for msg in messages:\n c = msg.get('content', '')\n if msg['role'] == 'system': \n self.set_system(c); continue\n if isinstance(c, str): combined_content.append({\"type\": \"text\", \"text\": c})\n elif isinstance(c, list): combined_content.extend(c)\n if msg['role'] == 'user' and msg.get('tool_results'): tool_results.extend(msg['tool_results'])\n tr_id_set = set(); tool_result_blocks = []\n for tr in tool_results:\n tool_use_id, content = tr.get(\"tool_use_id\", \"\"), tr.get(\"content\", \"\")\n tr_id_set.add(tool_use_id)\n if tool_use_id: tool_result_blocks.append({\"type\": \"tool_result\", \"tool_use_id\": tool_use_id, \"content\": tr.get(\"content\", \"\")})\n else: combined_content = [{\"type\": \"text\", \"text\": f'<tool_result>{content}</tool_result>'}] + combined_content\n for tid in self._pending_tool_ids:\n if tid not in tr_id_set: tool_result_blocks.append({\"type\": \"tool_result\", \"tool_use_id\": tid, \"content\": \"\"})\n self._pending_tool_ids = []\n # Filter whitespace-only text blocks that cause 400 on strict API proxies\n filtered_content = [c for c in combined_content if c.get(\"text\", \"\").strip()]\n final_content = tool_result_blocks + filtered_content\n if not final_content: final_content = [{\"type\": \"text\", \"text\": \".\"}]\n merged = {\"role\": \"user\", \"content\": final_content}\n _write_llm_log('Prompt', json.dumps(merged, ensure_ascii=False, indent=2), self.log_path)\n gen = self.backend.ask(merged)\n try:\n while True: \n chunk = next(gen); yield chunk\n except StopIteration as e: resp = e.value\n if resp: _write_llm_log('Response', resp.raw, self.log_path)\n if resp and hasattr(resp, 'tool_calls') and resp.tool_calls: self._pending_tool_ids = [tc.id for tc in resp.tool_calls]\n return resp\n\ndef resolve_session(cfg_name):\n cfg = reload_mykeys()[0].get(cfg_name)\n if not cfg: raise ValueError(f\"Config '{cfg_name}' not in mykey\")\n if 'native' in cfg_name: return (NativeClaudeSession if 'claude' in cfg_name else NativeOAISession)(cfg=cfg)\n if 'claude' in cfg_name: return ClaudeSession(cfg=cfg)\n return LLMSession(cfg=cfg) if 'oai' in cfg_name else None\n\ndef resolve_client(cfg_name):\n s = resolve_session(cfg_name)\n return (NativeToolClient(s) if isinstance(s, (NativeClaudeSession, NativeOAISession)) else ToolClient(s)) if s else None\n\ndef fast_ask(prompt, cfg_name):\n sess = resolve_session(cfg_name)\n if not sess: raise ValueError(f\"fast_ask: '{cfg_name}' unsupported\")\n return \"\".join(sess.raw_ask([{\"role\": \"user\", \"content\": prompt}]))\n"}}}}