chcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chcode/__init__.py +0 -0
- chcode/__main__.py +5 -0
- chcode/agent_setup.py +395 -0
- chcode/agents/__init__.py +0 -0
- chcode/agents/definitions.py +158 -0
- chcode/agents/loader.py +104 -0
- chcode/agents/runner.py +159 -0
- chcode/chat.py +1630 -0
- chcode/cli.py +142 -0
- chcode/config.py +571 -0
- chcode/display.py +325 -0
- chcode/prompts.py +640 -0
- chcode/session.py +149 -0
- chcode/skill_manager.py +165 -0
- chcode/utils/__init__.py +3 -0
- chcode/utils/enhanced_chat_openai.py +368 -0
- chcode/utils/git_checker.py +38 -0
- chcode/utils/git_manager.py +261 -0
- chcode/utils/modelscope_ratelimit.py +65 -0
- chcode/utils/multimodal.py +268 -0
- chcode/utils/shell/__init__.py +17 -0
- chcode/utils/shell/output.py +63 -0
- chcode/utils/shell/provider.py +128 -0
- chcode/utils/shell/result.py +14 -0
- chcode/utils/shell/semantics.py +55 -0
- chcode/utils/shell/session.py +159 -0
- chcode/utils/skill_loader.py +565 -0
- chcode/utils/text_utils.py +14 -0
- chcode/utils/tool_result_pipeline.py +244 -0
- chcode/utils/tools.py +1724 -0
- chcode/vision_config.py +371 -0
- chcode-0.1.0.dist-info/METADATA +275 -0
- chcode-0.1.0.dist-info/RECORD +36 -0
- chcode-0.1.0.dist-info/WHEEL +4 -0
- chcode-0.1.0.dist-info/entry_points.txt +2 -0
- chcode-0.1.0.dist-info/licenses/LICENSE +21 -0
chcode/cli.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI 入口 — Typer 应用
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _setup_langsmith_guard():
|
|
13
|
+
"""自动检测 LangSmith 429 并禁用追踪,防止 stderr 污染终端 UI"""
|
|
14
|
+
_disabled = False
|
|
15
|
+
|
|
16
|
+
class _Guard:
|
|
17
|
+
def __init__(self, original):
|
|
18
|
+
self._original = original
|
|
19
|
+
|
|
20
|
+
def write(self, data):
|
|
21
|
+
nonlocal _disabled
|
|
22
|
+
if not data:
|
|
23
|
+
return 0
|
|
24
|
+
if _disabled and ("LangSmith" in data or "langsmith" in data.lower()):
|
|
25
|
+
return len(data)
|
|
26
|
+
if "LangSmithRateLimitError" in data or (
|
|
27
|
+
"langsmith" in data.lower() and "429" in data
|
|
28
|
+
):
|
|
29
|
+
_disabled = True
|
|
30
|
+
os.environ["LANGCHAIN_TRACING_V2"] = "false"
|
|
31
|
+
return len(data)
|
|
32
|
+
if "langsmith" in data.lower() and (
|
|
33
|
+
"ConnectionError" in data
|
|
34
|
+
or "MaxRetryError" in data
|
|
35
|
+
or "ProtocolError" in data
|
|
36
|
+
or "Failed to send" in data
|
|
37
|
+
or "Connection aborted" in data
|
|
38
|
+
or "ConnectionAbortedError" in data
|
|
39
|
+
or "ConnectionResetError" in data
|
|
40
|
+
or "api.smith.langchain.com" in data
|
|
41
|
+
):
|
|
42
|
+
_disabled = True
|
|
43
|
+
os.environ["LANGCHAIN_TRACING_V2"] = "false"
|
|
44
|
+
return len(data)
|
|
45
|
+
return self._original.write(data)
|
|
46
|
+
|
|
47
|
+
def flush(self):
|
|
48
|
+
self._original.flush()
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, name):
|
|
51
|
+
return getattr(self._original, name)
|
|
52
|
+
|
|
53
|
+
_original = sys.__stderr__ or sys.stderr
|
|
54
|
+
_guard = _Guard(_original)
|
|
55
|
+
sys.stderr = _guard
|
|
56
|
+
sys.__stderr__ = _guard
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_setup_langsmith_guard()
|
|
60
|
+
|
|
61
|
+
import typer # noqa: E402
|
|
62
|
+
from rich.console import Console # noqa: E402
|
|
63
|
+
|
|
64
|
+
app = typer.Typer(
|
|
65
|
+
name="chcode",
|
|
66
|
+
help="Terminal-based AI coding agent",
|
|
67
|
+
no_args_is_help=False,
|
|
68
|
+
)
|
|
69
|
+
console = Console()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.callback(invoke_without_command=True)
|
|
73
|
+
def main(
|
|
74
|
+
ctx: typer.Context,
|
|
75
|
+
yolo: bool = typer.Option(
|
|
76
|
+
False, "--yolo", "-y", help="启用 Yolo 模式(自动批准所有操作)"
|
|
77
|
+
),
|
|
78
|
+
version: bool = typer.Option(False, "--version", "-v", help="显示版本"),
|
|
79
|
+
):
|
|
80
|
+
"""ChCode — 终端 AI 编程助手"""
|
|
81
|
+
if version:
|
|
82
|
+
console.print("chcode v0.1.0")
|
|
83
|
+
raise typer.Exit()
|
|
84
|
+
|
|
85
|
+
if ctx.invoked_subcommand is not None:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
asyncio.run(_run_chat(yolo))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def _run_chat(yolo: bool) -> None:
|
|
92
|
+
from chcode.chat import ChatREPL
|
|
93
|
+
|
|
94
|
+
repl = ChatREPL()
|
|
95
|
+
repl.yolo = yolo
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
ok = await repl.initialize()
|
|
99
|
+
except Exception:
|
|
100
|
+
console.print_exception()
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
if not ok:
|
|
104
|
+
console.print("[red]初始化失败[/red]")
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
await repl.run()
|
|
109
|
+
finally:
|
|
110
|
+
await repl.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def config(
|
|
115
|
+
action: str = typer.Argument("edit", help="edit | new | switch"),
|
|
116
|
+
):
|
|
117
|
+
"""模型配置管理"""
|
|
118
|
+
asyncio.run(_run_config(action))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _run_config(action: str) -> None:
|
|
122
|
+
from chcode.config import configure_new_model, edit_current_model, switch_model
|
|
123
|
+
|
|
124
|
+
if action == "new":
|
|
125
|
+
await configure_new_model()
|
|
126
|
+
elif action == "edit":
|
|
127
|
+
await edit_current_model()
|
|
128
|
+
elif action == "switch":
|
|
129
|
+
await switch_model()
|
|
130
|
+
else:
|
|
131
|
+
console.print(f"[yellow]未知操作: {action}[/yellow]")
|
|
132
|
+
console.print("可用操作: new, edit, switch")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.command()
|
|
136
|
+
def version():
|
|
137
|
+
"""显示版本"""
|
|
138
|
+
console.print("chcode v0.1.0")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
app() # pragma: no cover
|
chcode/config.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
模型配置管理 — 读取/保存 model.json,切换模型
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
from chcode.prompts import select, confirm, model_config_form, text, configure_longcat
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
CONFIG_DIR = Path.home() / ".chat"
|
|
21
|
+
MODEL_JSON = CONFIG_DIR / "model.json"
|
|
22
|
+
SETTING_JSON = CONFIG_DIR / "chagent.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ENV_TO_CONFIG: dict[str, dict[str, str | list[str]]] = {
|
|
26
|
+
"BIGMODEL_API_KEY": {
|
|
27
|
+
"name": "智谱 GLM",
|
|
28
|
+
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
29
|
+
"models": ["glm-4.7", "glm-5","glm-5-turbo","glm-5.1"],
|
|
30
|
+
},
|
|
31
|
+
"OPENAI_API_KEY": {
|
|
32
|
+
"name": "OpenAI",
|
|
33
|
+
"base_url": "https://api.openai.com/v1",
|
|
34
|
+
"models": ["gpt-5.4", "gpt-5.3"],
|
|
35
|
+
},
|
|
36
|
+
"DEEPSEEK_API_KEY": {
|
|
37
|
+
"name": "DeepSeek",
|
|
38
|
+
"base_url": "https://api.deepseek.com/v1",
|
|
39
|
+
"models": ["deepseek-chat"],
|
|
40
|
+
},
|
|
41
|
+
"DASHSCOPE_API_KEY": {
|
|
42
|
+
"name": "通义千问",
|
|
43
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
44
|
+
"models": ["qwen3.5-plus", "qwen-turbo"],
|
|
45
|
+
},
|
|
46
|
+
"ModelScopeToken": {
|
|
47
|
+
"name": "ModelScope",
|
|
48
|
+
"base_url": "https://api-inference.modelscope.cn/v1",
|
|
49
|
+
"models": ["Qwen/Qwen3-235B-A22B-Thinking-2507"],
|
|
50
|
+
},
|
|
51
|
+
"ANTHROPIC_API_KEY": {
|
|
52
|
+
"name": "Anthropic Claude",
|
|
53
|
+
"base_url": "https://api.anthropic.com/v1",
|
|
54
|
+
"models": ["claude-sonnet-4.6"],
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# 确保.chat配置目录存在
|
|
59
|
+
def ensure_config_dir() -> Path:
|
|
60
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
61
|
+
return CONFIG_DIR
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_model_json_cache: tuple[float, dict] | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_model_json() -> dict:
|
|
68
|
+
"""加载 model.json,带 mtime 缓存"""
|
|
69
|
+
global _model_json_cache
|
|
70
|
+
if not MODEL_JSON.exists():
|
|
71
|
+
return {}
|
|
72
|
+
try:
|
|
73
|
+
mtime = MODEL_JSON.stat().st_mtime
|
|
74
|
+
if _model_json_cache and _model_json_cache[0] == mtime:
|
|
75
|
+
return _model_json_cache[1]
|
|
76
|
+
data = json.loads(MODEL_JSON.read_text(encoding="utf-8"))
|
|
77
|
+
_model_json_cache = (mtime, data)
|
|
78
|
+
return data
|
|
79
|
+
except Exception:
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save_model_json(data: dict) -> None:
|
|
84
|
+
global _model_json_cache
|
|
85
|
+
MODEL_JSON.write_text(
|
|
86
|
+
json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
_model_json_cache = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_default_model_config() -> dict | None:
|
|
92
|
+
"""获取当前默认模型配置"""
|
|
93
|
+
data = load_model_json()
|
|
94
|
+
return data.get("default") or None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def detect_env_api_keys() -> list[dict]:
|
|
98
|
+
"""检测环境变量中的 API Key,返回推荐配置列表"""
|
|
99
|
+
results = []
|
|
100
|
+
for var, cfg in ENV_TO_CONFIG.items():
|
|
101
|
+
key = os.getenv(var, "")
|
|
102
|
+
if key:
|
|
103
|
+
results.append({"env_var": var, "api_key": key, **cfg})
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def first_run_configure() -> dict | None:
|
|
108
|
+
"""首次运行配置引导"""
|
|
109
|
+
console.print()
|
|
110
|
+
console.print(
|
|
111
|
+
Panel(
|
|
112
|
+
"[bold]ChCode[/bold] — 终端 AI 编程助手\n\n"
|
|
113
|
+
"首次运行需要配置 AI 模型连接。\n"
|
|
114
|
+
"设置环境变量后可自动检测(推荐),或手动填写配置。",
|
|
115
|
+
border_style="cyan",
|
|
116
|
+
padding=(1, 2),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
console.print()
|
|
120
|
+
|
|
121
|
+
detected = detect_env_api_keys()
|
|
122
|
+
|
|
123
|
+
if detected:
|
|
124
|
+
choices = [f"{d['name']} (检测到 {d['env_var']})" for d in detected]
|
|
125
|
+
choices.append("魔搭快捷配置...")
|
|
126
|
+
choices.append("LongCat 快捷配置...")
|
|
127
|
+
choices.append("手动配置...")
|
|
128
|
+
choices.append("退出")
|
|
129
|
+
|
|
130
|
+
result = await select("选择配置方式:", choices)
|
|
131
|
+
if result is None or "退出" in result:
|
|
132
|
+
console.print(
|
|
133
|
+
"[dim]设置环境变量后重新运行,或执行 chcode config new 手动配置[/dim]"
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
if "手动" in result:
|
|
138
|
+
return await configure_new_model()
|
|
139
|
+
|
|
140
|
+
if "魔搭" in result:
|
|
141
|
+
return await _configure_modelscope_with_test()
|
|
142
|
+
|
|
143
|
+
if "LongCat" in result:
|
|
144
|
+
return await _configure_longcat_with_test()
|
|
145
|
+
|
|
146
|
+
idx = choices.index(result)
|
|
147
|
+
chosen = detected[idx]
|
|
148
|
+
|
|
149
|
+
model_list = chosen["models"]
|
|
150
|
+
model = await select("选择模型:", model_list)
|
|
151
|
+
if model is None:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
config: dict[str, Any] = {
|
|
155
|
+
"model": model,
|
|
156
|
+
"base_url": chosen["base_url"],
|
|
157
|
+
"api_key": chosen["api_key"],
|
|
158
|
+
"stream_usage": True,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.print("[yellow]测试连接中...[/yellow]")
|
|
162
|
+
try:
|
|
163
|
+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
|
|
164
|
+
|
|
165
|
+
model_inst = EnhancedChatOpenAI(**config)
|
|
166
|
+
await asyncio.to_thread(model_inst.invoke, "你好")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
if "null value" not in str(e):
|
|
169
|
+
console.print(f"[red]连接失败: {e}[/red]")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
data = load_model_json()
|
|
173
|
+
old_default = data.get("default")
|
|
174
|
+
fallback = data.get("fallback", {})
|
|
175
|
+
if old_default:
|
|
176
|
+
old_name = old_default.get("model", "")
|
|
177
|
+
if old_name and old_name not in fallback:
|
|
178
|
+
fallback[old_name] = old_default
|
|
179
|
+
data["default"] = config
|
|
180
|
+
data["fallback"] = fallback
|
|
181
|
+
save_model_json(data)
|
|
182
|
+
console.print(f"[green]配置完成: {model}[/green]")
|
|
183
|
+
|
|
184
|
+
await configure_tavily()
|
|
185
|
+
return config
|
|
186
|
+
else:
|
|
187
|
+
console.print("[yellow]未检测到环境变量中的 API Key[/yellow]")
|
|
188
|
+
choices = ["魔搭快捷配置...", "LongCat 快捷配置...", "手动配置...", "退出"]
|
|
189
|
+
result = await select("选择:", choices)
|
|
190
|
+
if result is None or "退出" in result:
|
|
191
|
+
console.print("[dim]提示: 在环境变量中设置 API Key 后重新运行,例如:[/dim]")
|
|
192
|
+
console.print("[dim] set BIGMODEL_API_KEY=your_key[/dim]")
|
|
193
|
+
console.print("[dim]或执行 chcode config new 手动配置[/dim]")
|
|
194
|
+
return None
|
|
195
|
+
if "魔搭" in result:
|
|
196
|
+
return await _configure_modelscope_with_test()
|
|
197
|
+
if "LongCat" in result:
|
|
198
|
+
return await _configure_longcat_with_test()
|
|
199
|
+
return await configure_new_model()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def configure_new_model() -> dict | None:
|
|
203
|
+
"""新建模型配置(交互式表单)"""
|
|
204
|
+
ensure_config_dir()
|
|
205
|
+
result = await select("配置方式:", ["魔搭快捷配置...", "LongCat 快捷配置...", "手动配置..."])
|
|
206
|
+
if result is None:
|
|
207
|
+
return None
|
|
208
|
+
if "魔搭" in result:
|
|
209
|
+
return await _configure_modelscope_with_test()
|
|
210
|
+
if "LongCat" in result:
|
|
211
|
+
return await _configure_longcat_with_test()
|
|
212
|
+
config = await model_config_form()
|
|
213
|
+
if config is None:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
# 测试连接
|
|
217
|
+
console.print("[yellow]测试连接中...[/yellow]")
|
|
218
|
+
try:
|
|
219
|
+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
|
|
220
|
+
|
|
221
|
+
model = EnhancedChatOpenAI(**config)
|
|
222
|
+
await asyncio.to_thread(model.invoke, "你好")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
import traceback
|
|
225
|
+
|
|
226
|
+
err_msg = str(e)
|
|
227
|
+
if "null value for 'choices'" not in err_msg:
|
|
228
|
+
console.print(f"[red]连接测试失败: {err_msg}[/red]")
|
|
229
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
230
|
+
return None
|
|
231
|
+
data = load_model_json()
|
|
232
|
+
old_default = data.get("default")
|
|
233
|
+
fallback = data.get("fallback", {})
|
|
234
|
+
|
|
235
|
+
if not old_default:
|
|
236
|
+
# 第一次配置 — 直接设为默认
|
|
237
|
+
data["default"] = config
|
|
238
|
+
data["fallback"] = {}
|
|
239
|
+
else:
|
|
240
|
+
# 已有默认 — 新模型设为默认,旧默认移到 fallback
|
|
241
|
+
old_name = old_default.get("model", "")
|
|
242
|
+
if old_name and old_name not in fallback:
|
|
243
|
+
fallback[old_name] = old_default
|
|
244
|
+
data["default"] = config
|
|
245
|
+
data["fallback"] = fallback
|
|
246
|
+
|
|
247
|
+
save_model_json(data)
|
|
248
|
+
console.print(f"[green]模型配置已保存: {config['model']}[/green]")
|
|
249
|
+
|
|
250
|
+
await configure_tavily()
|
|
251
|
+
return config
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def _configure_modelscope_with_test() -> dict | None:
|
|
255
|
+
"""魔搭快捷配置:收集 API Key → 测试连接 → 保存 12 个预定义模型。"""
|
|
256
|
+
from chcode.prompts import configure_modelscope
|
|
257
|
+
|
|
258
|
+
ms_config = await configure_modelscope()
|
|
259
|
+
if ms_config is None:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
default = ms_config["default"]
|
|
263
|
+
|
|
264
|
+
# 测试连接
|
|
265
|
+
console.print("[yellow]测试连接中...[/yellow]")
|
|
266
|
+
try:
|
|
267
|
+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
|
|
268
|
+
|
|
269
|
+
model_inst = EnhancedChatOpenAI(**default)
|
|
270
|
+
await asyncio.to_thread(model_inst.invoke, "你好")
|
|
271
|
+
except Exception as e:
|
|
272
|
+
import traceback
|
|
273
|
+
|
|
274
|
+
err_msg = str(e)
|
|
275
|
+
if "null value for 'choices'" not in err_msg:
|
|
276
|
+
console.print(f"[red]连接测试失败: {err_msg}[/red]")
|
|
277
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
# 合并到已有配置,保留非魔搭的已有模型
|
|
281
|
+
data = load_model_json()
|
|
282
|
+
old_default = data.get("default")
|
|
283
|
+
existing_fallback = data.get("fallback", {})
|
|
284
|
+
|
|
285
|
+
if not old_default:
|
|
286
|
+
# 首次配置 — 魔搭直接作为完整配置
|
|
287
|
+
save_model_json(ms_config)
|
|
288
|
+
else:
|
|
289
|
+
# 已有配置 — 旧的 default 移入 fallback,魔搭作为新 default,合并 fallback
|
|
290
|
+
if old_default["model"] not in existing_fallback:
|
|
291
|
+
existing_fallback[old_default["model"]] = old_default
|
|
292
|
+
existing_fallback.update(ms_config["fallback"])
|
|
293
|
+
data["default"] = ms_config["default"]
|
|
294
|
+
data["fallback"] = existing_fallback
|
|
295
|
+
save_model_json(data)
|
|
296
|
+
fallback_names = ", ".join(ms_config["fallback"].keys())
|
|
297
|
+
console.print(f"[green]配置完成: {default['model']} (默认)[/green]")
|
|
298
|
+
console.print(f"[dim]备用模型 ({len(ms_config['fallback'])} 个): {fallback_names}[/dim]")
|
|
299
|
+
|
|
300
|
+
# 魔搭配置完成后,自动同步视觉模型配置
|
|
301
|
+
from chcode.vision_config import auto_configure_vision
|
|
302
|
+
vision_default = auto_configure_vision()
|
|
303
|
+
if vision_default:
|
|
304
|
+
console.print(f"[dim]视觉模型已自动配置: {vision_default.get('model', '未知')}[/dim]")
|
|
305
|
+
|
|
306
|
+
await configure_tavily()
|
|
307
|
+
return default
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def _configure_longcat_with_test() -> dict | None:
|
|
311
|
+
"""LongCat 快捷配置:收集 API Key → 测试连接 → 保存 4 个预定义模型。"""
|
|
312
|
+
lc_config = await configure_longcat()
|
|
313
|
+
if lc_config is None:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
default = lc_config["default"]
|
|
317
|
+
|
|
318
|
+
# 测试连接
|
|
319
|
+
console.print("[yellow]测试连接中...[/yellow]")
|
|
320
|
+
try:
|
|
321
|
+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
|
|
322
|
+
|
|
323
|
+
model_inst = EnhancedChatOpenAI(**default)
|
|
324
|
+
await asyncio.to_thread(model_inst.invoke, "你好")
|
|
325
|
+
except Exception as e:
|
|
326
|
+
import traceback
|
|
327
|
+
|
|
328
|
+
err_msg = str(e)
|
|
329
|
+
if "null value for 'choices'" not in err_msg:
|
|
330
|
+
console.print(f"[red]连接测试失败: {err_msg}[/red]")
|
|
331
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
# 合并到已有配置,保留非 LongCat 的已有模型
|
|
335
|
+
data = load_model_json()
|
|
336
|
+
old_default = data.get("default")
|
|
337
|
+
existing_fallback = data.get("fallback", {})
|
|
338
|
+
|
|
339
|
+
if not old_default:
|
|
340
|
+
save_model_json(lc_config)
|
|
341
|
+
else:
|
|
342
|
+
if old_default["model"] not in existing_fallback:
|
|
343
|
+
existing_fallback[old_default["model"]] = old_default
|
|
344
|
+
existing_fallback.update(lc_config["fallback"])
|
|
345
|
+
data["default"] = lc_config["default"]
|
|
346
|
+
data["fallback"] = existing_fallback
|
|
347
|
+
save_model_json(data)
|
|
348
|
+
fallback_names = ", ".join(lc_config["fallback"].keys())
|
|
349
|
+
console.print(f"[green]配置完成: {default['model']} (默认)[/green]")
|
|
350
|
+
console.print(f"[dim]备用模型 ({len(lc_config['fallback'])} 个): {fallback_names}[/dim]")
|
|
351
|
+
|
|
352
|
+
await configure_tavily()
|
|
353
|
+
return default
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
async def edit_current_model() -> dict | None:
|
|
357
|
+
"""编辑当前默认模型"""
|
|
358
|
+
data = load_model_json()
|
|
359
|
+
current = data.get("default", {})
|
|
360
|
+
if not current:
|
|
361
|
+
console.print("[yellow]没有当前模型配置,请新建[/yellow]")
|
|
362
|
+
return await configure_new_model()
|
|
363
|
+
|
|
364
|
+
config = await model_config_form(existing_config=current)
|
|
365
|
+
if config is None:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
# 测试连接
|
|
369
|
+
console.print("[yellow]测试连接中...[/yellow]")
|
|
370
|
+
try:
|
|
371
|
+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
|
|
372
|
+
|
|
373
|
+
model = EnhancedChatOpenAI(**config)
|
|
374
|
+
await asyncio.to_thread(model.invoke, "你好")
|
|
375
|
+
except Exception as e:
|
|
376
|
+
import traceback
|
|
377
|
+
|
|
378
|
+
err_msg = str(e)
|
|
379
|
+
if "null value for 'choices'" not in err_msg:
|
|
380
|
+
console.print(f"[red]连接测试失败: {err_msg}[/red]")
|
|
381
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
382
|
+
return None
|
|
383
|
+
data["default"] = config
|
|
384
|
+
save_model_json(data)
|
|
385
|
+
console.print(f"[green]模型配置已更新: {config['model']}[/green]")
|
|
386
|
+
return config
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def switch_model() -> dict | None:
|
|
390
|
+
"""切换模型(从 fallback 列表选择)"""
|
|
391
|
+
data = load_model_json()
|
|
392
|
+
default = data.get("default", {})
|
|
393
|
+
fallback = data.get("fallback", {})
|
|
394
|
+
|
|
395
|
+
if not default:
|
|
396
|
+
console.print("[yellow]请先配置默认模型[/yellow]")
|
|
397
|
+
return await configure_new_model()
|
|
398
|
+
|
|
399
|
+
if not fallback:
|
|
400
|
+
console.print("[yellow]没有备用模型可切换[/yellow]")
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
# 构建选项列表
|
|
404
|
+
current_name = default.get("model", "")
|
|
405
|
+
choices = []
|
|
406
|
+
for name in fallback:
|
|
407
|
+
tag = " (当前默认)" if name == current_name else ""
|
|
408
|
+
choices.append(f"{name}{tag}")
|
|
409
|
+
|
|
410
|
+
result = await select("选择要使用的模型:", choices)
|
|
411
|
+
if result is None:
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
# 提取模型名(去掉 " (当前默认)" 后缀)
|
|
415
|
+
selected_name = result.replace(" (当前默认)", "")
|
|
416
|
+
|
|
417
|
+
ok = await confirm(f"确定切换到 {selected_name}?当前默认将移至备用列表")
|
|
418
|
+
if not ok:
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
selected_config = fallback.pop(selected_name)
|
|
422
|
+
if default and current_name not in fallback:
|
|
423
|
+
fallback[current_name] = default
|
|
424
|
+
|
|
425
|
+
data["default"] = selected_config
|
|
426
|
+
data["fallback"] = fallback
|
|
427
|
+
save_model_json(data)
|
|
428
|
+
console.print(f"[green]已切换到: {selected_name}[/green]")
|
|
429
|
+
return selected_config
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def load_workplace() -> Path | None:
|
|
433
|
+
"""加载上次的工作目录"""
|
|
434
|
+
if SETTING_JSON.exists():
|
|
435
|
+
try:
|
|
436
|
+
data = json.loads(SETTING_JSON.read_text(encoding="utf-8"))
|
|
437
|
+
wp = data.get("workplace_path", "")
|
|
438
|
+
if wp:
|
|
439
|
+
return Path(wp)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def save_workplace(path: Path) -> None:
|
|
446
|
+
ensure_config_dir()
|
|
447
|
+
data = {}
|
|
448
|
+
if SETTING_JSON.exists():
|
|
449
|
+
try:
|
|
450
|
+
data = json.loads(SETTING_JSON.read_text(encoding="utf-8"))
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
453
|
+
data["workplace_path"] = str(path)
|
|
454
|
+
SETTING_JSON.write_text(
|
|
455
|
+
json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def load_tavily_api_key() -> str:
|
|
460
|
+
"""加载 Tavily API Key"""
|
|
461
|
+
if SETTING_JSON.exists():
|
|
462
|
+
try:
|
|
463
|
+
data = json.loads(SETTING_JSON.read_text(encoding="utf-8"))
|
|
464
|
+
return data.get("tavily_api_key", "")
|
|
465
|
+
except Exception:
|
|
466
|
+
pass
|
|
467
|
+
return os.getenv("TAVILY_API_KEY", "")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def save_tavily_api_key(api_key: str) -> None:
|
|
471
|
+
"""保存 Tavily API Key"""
|
|
472
|
+
ensure_config_dir()
|
|
473
|
+
data = {}
|
|
474
|
+
if SETTING_JSON.exists():
|
|
475
|
+
try:
|
|
476
|
+
data = json.loads(SETTING_JSON.read_text(encoding="utf-8"))
|
|
477
|
+
except Exception:
|
|
478
|
+
pass
|
|
479
|
+
data["tavily_api_key"] = api_key
|
|
480
|
+
SETTING_JSON.write_text(
|
|
481
|
+
json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# ─── 上下文窗口大小 ──────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
CONTEXT_WINDOW_SIZES: dict[str, int] = {
|
|
488
|
+
"gpt-4o": 128000,
|
|
489
|
+
"gpt-4o-mini": 128000,
|
|
490
|
+
"claude-sonnet-4-20250514": 200000,
|
|
491
|
+
"deepseek-chat": 65536,
|
|
492
|
+
"deepseek-v3.2": 128000,
|
|
493
|
+
"deepseek-r1-0528": 65536,
|
|
494
|
+
"glm-5.1": 200000,
|
|
495
|
+
"glm-5": 200000,
|
|
496
|
+
"glm-4.7": 200000,
|
|
497
|
+
"minimax-m2": 204800,
|
|
498
|
+
"minimax-m2.5": 200000,
|
|
499
|
+
"kimi-k2": 256000,
|
|
500
|
+
"mimo-v2-flash": 256000,
|
|
501
|
+
"qwen3.5-plus": 1000000,
|
|
502
|
+
"qwen3.6-plus": 1000000,
|
|
503
|
+
"qwen": 256000,
|
|
504
|
+
"longcat-2.0-preview": 1000000,
|
|
505
|
+
"longcat-flash-chat": 262144,
|
|
506
|
+
"longcat-flash-thinking": 262144,
|
|
507
|
+
"longcat-flash-lite": 500000,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_DEFAULT_CONTEXT_WINDOW = 256000
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def get_context_window_size(model_name: str) -> int:
|
|
514
|
+
"""根据模型名获取上下文窗口大小,无匹配时返回默认值"""
|
|
515
|
+
if not model_name:
|
|
516
|
+
return _DEFAULT_CONTEXT_WINDOW
|
|
517
|
+
# 精确匹配
|
|
518
|
+
if model_name in CONTEXT_WINDOW_SIZES:
|
|
519
|
+
return CONTEXT_WINDOW_SIZES[model_name]
|
|
520
|
+
# 前缀匹配(去掉 org/ 前缀后匹配)
|
|
521
|
+
short = model_name.split("/")[-1].lower()
|
|
522
|
+
if short in CONTEXT_WINDOW_SIZES:
|
|
523
|
+
return CONTEXT_WINDOW_SIZES[short]
|
|
524
|
+
for key, size in CONTEXT_WINDOW_SIZES.items():
|
|
525
|
+
if key in model_name.lower():
|
|
526
|
+
return size
|
|
527
|
+
return _DEFAULT_CONTEXT_WINDOW
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
async def configure_tavily() -> None:
|
|
531
|
+
"""首次引导时配置 Tavily"""
|
|
532
|
+
tavily_env = os.getenv("TAVILY_API_KEY")
|
|
533
|
+
|
|
534
|
+
if tavily_env:
|
|
535
|
+
save_tavily_api_key(tavily_env)
|
|
536
|
+
from chcode.utils.tools import update_tavily_api_key
|
|
537
|
+
|
|
538
|
+
update_tavily_api_key(tavily_env)
|
|
539
|
+
console.print("[dim]检测到 TAVILY_API_KEY 环境变量,已自动配置 Tavily[/dim]")
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
if SETTING_JSON.exists():
|
|
543
|
+
try:
|
|
544
|
+
data = json.loads(SETTING_JSON.read_text(encoding="utf-8"))
|
|
545
|
+
current = data.get("tavily_api_key", "")
|
|
546
|
+
if current:
|
|
547
|
+
from chcode.utils.tools import update_tavily_api_key
|
|
548
|
+
|
|
549
|
+
update_tavily_api_key(current)
|
|
550
|
+
console.print(
|
|
551
|
+
f"[dim]已配置 Tavily: {current[:6]}...{current[-4:]}[/dim]"
|
|
552
|
+
)
|
|
553
|
+
return
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
console.print()
|
|
558
|
+
result = await select("是否配置 Tavily 搜索引擎?", ["是", "否"])
|
|
559
|
+
if result is None or result == "否":
|
|
560
|
+
console.print("[dim]已跳过,后续可通过 /search 命令配置[/dim]")
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
new_key = await text("请输入 Tavily API Key:")
|
|
564
|
+
if new_key:
|
|
565
|
+
save_tavily_api_key(new_key)
|
|
566
|
+
from chcode.utils.tools import update_tavily_api_key
|
|
567
|
+
|
|
568
|
+
update_tavily_api_key(new_key)
|
|
569
|
+
console.print("[green]Tavily API Key 已保存并生效[/green]")
|
|
570
|
+
else:
|
|
571
|
+
console.print("[dim]已取消[/dim]")
|