{"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-',\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-',\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-',\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-',\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-',\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-',\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-',\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...',\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': '无 标签,原生 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-',\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': '',\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': '',\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-',\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-',\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-',\n 'apibase': 'https:///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-',\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-',\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_',\n 'apibase': 'https:///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:///api'},\n {'id': 'gemini_ultra', 'name': 'Gemini Ultra (Antigravity)', 'desc': 'CRS 包装的 Google Antigravity,不支持 SSE 流式', 'apibase': 'https:///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': '',\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-',\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-',\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-',\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-',\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-',\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-',\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-',\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...',\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': '无 标签,原生 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-',\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': '',\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': '',\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-',\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-',\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-',\n 'apibase': 'https:///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-',\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-',\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_',\n 'apibase': 'https:///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:///api'},\n {'id': 'gemini_ultra', 'name': 'Gemini Ultra (Antigravity)', 'desc': 'CRS 包装的 Google Antigravity,不支持 SSE 流式', 'apibase': 'https:///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': '',\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.=` 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-',\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-',\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 // 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]*?)()') for tag in ('thinking', 'think', 'tool_use', 'tool_result')}\n _hist_pat = re.compile(r'<(history|key_info|earlier_context)>[\\s\\S]*?')\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)}>[...]', 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'{json.dumps(tu, ensure_ascii=False)}'\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\"(.*?)\"\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\"\"\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 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 `` tags.\n2. **Summarize**: Output a minimal one-line (<30 words) physical snapshot in ``: 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 ** blocks** after your reply, then stop.\n\"\"\"\n else:\n tool_instruction = f\"\"\"\n### 交互协议 (必须严格遵守,持续有效)\n请按照以下步骤思考并行动:\n1. **思考**: 在 `` 标签中先进行思考,分析现状和策略。\n2. **总结**: 在 `` 中输出*极为简短*的高度概括的单行(<30字)物理快照,包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。\n3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**块**,然后结束。\n\"\"\"\n tool_instruction += f'\\nFormat: ```{{\"name\": \"tool_name\", \"arguments\": {{...}}}}```\\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'{tr[\"content\"]}\\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\"(.*?)\", text, re.DOTALL)\n if think_match:\n thinking = think_match.group(1).strip()\n remaining_text = re.sub(r\"(.*?)\", \"\", 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 '' in remaining_text:\n weaktoolstr = remaining_text.split('')[-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(''+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: {\"name\":..., \"arguments\":...}\n _xp = r\"<(?:tool_use|tool_call)>((?:(?!<(?:tool_use|tool_call)>).){15,}?)\"\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 = \"\" + (line[:60] + '...' if len(line) > 60 else line) + \"\"\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每次回复(含工具调用轮)都先在回复文字中包含一个 中输出极简单行(<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 : 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'{content}'}] + 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"}}}}