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/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]")