1"""

2`agentos init` — 交互式配置向导。

3

4 功能：

5 - 检测当前配置状态

6 - 引导选择 Provider + 输入 API Key

7 - 写入 ~/.agentos/config.yaml

8 - 可选写入 .env 文件（当前或全局）

9

10 命令：

11 agentos init # 交互式引导

12 agentos init --quick # 跳过问答，直接生成 .env.example

13 agentos init --reset # 重置配置

14"""

15

16 from __future__ import annotations

17

18 import json

19 import os

20 import re

21 import sys

22 from pathlib import Path

23 from typing import Optional

24

25

26 CONFIG_DIR = Path.home() / ".agentos"

27 CONFIG_FILE = CONFIG_DIR / "config.yaml"

28 ENV_FILE = CONFIG_DIR / ".env"

29

30 PROVIDERS = {

31 "openai": {

32 "label": "OpenAI",

33 "models": ["gpt-4o-mini", "gpt-4o", "o3-mini"],

34 "default_model": "gpt-4o-mini",

35 "env_var": "OPENAI_API_KEY",

36 "key_prefix": "sk-",

37 "website": "https://platform.openai.com/api-keys",

38 "cost": "低 ~ 中",

39 },

40 "deepseek": {

41 "label": "DeepSeek",

42 "models": ["deepseek-chat", "deepseek-reasoner"],

43 "default_model": "deepseek-chat",

44 "env_var": "DEEPSEEK_API_KEY",

45 "key_prefix": "sk-",

46 "website": "https://platform.deepseek.com/api_keys",

47 "cost": "低",

48 },

49 "anthropic": {

50 "label": "Anthropic (Claude)",

51 "models": ["claude-sonnet-4", "claude-haiku-3-5", "claude-opus-4"],

52 "default_model": "claude-sonnet-4",

53 "env_var": "ANTHROPIC_API_KEY",

54 "key_prefix": "sk-ant-",

55 "website": "https://console.anthropic.com/keys",

56 "cost": "中 ~ 高",

57 },

58}

59

60

61 def _detect_current_config() -> dict:

62 """检测当前环境的配置状态。"""

63 config = {"providers": {}, "configured_providers": [], "active": None}

64

65 for name, info in PROVIDERS.items():

66 key = os.environ.get(info["env_var"]) or ""

67 masked = key[:8] + "..." + key[-4:] if len(key) > 20 else ""

68 config["providers"][name] = {

69 "env_set": bool(key),

70 "key_preview": masked,

71 }

72 if key:

73 config["configured_providers"].append(name)

74

75 # Check config file

76 if CONFIG_FILE.exists():

77 config["config_file_exists"] = True

78 try:

79 content = CONFIG_FILE.read_text()

80 for name in PROVIDERS:

81 if f"{PROVIDERS[name]['env_var']}:" in content:

82 config["providers"][name]["in_config"] = True

83 except Exception:

84 pass

85

86 # Determine active provider

87 for name in ["openai", "deepseek", "anthropic"]:

88 if config["providers"][name]["env_set"] or config["providers"].get(name, {}).get("in_config"):

89 config["active"] = name

90 break

91

92 return config

93

94

95 def _print_banner():

96 """打印欢迎横幅。"""

97 from agentos import __version__

98 print(f"\n ╔══════════════════════════════════════════════╗")

99 print(f" ║ Nexus AgentOS v{__version__:8s} ║")

100 print(f" ║ 交互式配置向导 ║")

101 print(f" ╚══════════════════════════════════════════════╝")

102 print()

103

104

105 def _print_status(config: dict):

106 """打印当前配置状态。"""

107 print(" ── 当前环境检测 ──")

108 print()

109 for name, info in config["providers"].items():

110 p = PROVIDERS[name]

111 status = "✅" if info["env_set"] else "⬜"

112 key_info = info.get("key_preview", "") or "未配置"

113 in_config = " (配置文件)" if info.get("in_config") else ""

114 print(f" {status} {p['label']:20s} {key_info:25s}{in_config}")

115 print()

116

117

118 def _select_provider() -> str:

119 """交互选择 Provider。"""

120 print(" ── 选择 LLM 服务商 ──")

121 print()

122 names = list(PROVIDERS.keys())

123 for i, name in enumerate(names, 1):

124 p = PROVIDERS[name]

125 print(f" [{i}] {p['label']:20s} 模型: {p['default_model']:15s} 成本: {p['cost']}")

126 print()

127

128 while True:

129 try:

130 choice = input(" 请选择 (1-3) [1]: ").strip()

131 if not choice:

132 return "openai"

133 idx = int(choice) - 1

134 if 0 <= idx < len(names):

135 return names[idx]

136 except ValueError:

137 pass

138 print(" 输入无效，请输入数字 1-3。")

139

140

141 def _input_api_key(provider_name: str) -> Optional[str]:

142 """交互输入 API Key。"""

143 p = PROVIDERS[provider_name]

144 print()

145 print(f" ── 配置 {p['label']} API Key ──")

146 print()

147 print(f" ① 打开 {p['website']}")

148 print(f" ② 创建或复制一个 API Key")

149 print(f" ③ 粘贴到下方（输入后按回车）")

150 print()

151

152 existing = os.environ.get(p["env_var"], "")

153 if existing:

154 preview = existing[:8] + "..." + existing[-4:] if len(existing) > 20 else existing

155 use_existing = input(f" 检测到环境变量已设置 ({preview})，直接使用？(Y/n): ").strip().lower()

156 if use_existing in ("", "y", "yes"):

157 return existing

158

159 while True:

160 key = input(f" 请输入 {p['label']} API Key: ").strip()

161 if not key:

162 print(" API Key 不能为空。输入 q 取消。")

163 continue

164 if key.lower() == "q":

165 return None

166 # Basic validation

167 prefix = p["key_prefix"]

168 if prefix and not key.startswith(prefix):

169 warn = input(f" 警告：{p['label']} 的 Key 通常以 '{prefix}' 开头，"

170 f"确认继续？(y/N): ").strip().lower()

171 if warn not in ("y", "yes"):

172 continue

173 return key

174

175

176 def _test_connection(provider_name: str, api_key: str) -> bool:

177 """测试 API 连接（发一条最轻的请求）。"""

178 p = PROVIDERS[provider_name]

179 print(f"\n 正在测试 {p['label']} API 连接...", end=" ")

180

181 try:

182 if provider_name == "openai":

183 import httpx

184 resp = httpx.get(

185 "https://api.openai.com/v1/models",

186 headers={"Authorization": f"Bearer {api_key}"},

187 timeout=10,

188 )

189 if resp.status_code == 200:

190 print("✅ 成功")

191 return True

192 elif resp.status_code == 401:

193 print("❌ Key 无效（401 Unauthorized）")

194 return False

195 else:

196 print(f"⚠️ 返回 {resp.status_code}，Key 格式正确但不一定可用")

197 return True

198 elif provider_name == "deepseek":

199 import httpx

200 resp = httpx.post(

201 "https://api.deepseek.com/chat/completions",

202 headers={

203 "Authorization": f"Bearer {api_key}",

204 "Content-Type": "application/json",

205 },

206 json={"model": "deepseek-chat", "messages": [{"role": "user",

207 "content": "hi"}], "max_tokens": 1},

208 timeout=10,

209 )

210 if resp.status_code == 200:

211 print("✅ 成功")

212 return True

213 elif resp.status_code == 401:

214 print("❌ Key 无效（401）")

215 return False

216 else:

217 print(f"⚠️ 返回 {resp.status_code}")

218 return True

219 elif provider_name == "anthropic":

220 import httpx

221 resp = httpx.post(

222 "https://api.anthropic.com/v1/messages",

223 headers={

224 "x-api-key": api_key,

225 "anthropic-version": "2023-06-01",

226 "Content-Type": "application/json",

227 },

228 json={"model": "claude-sonnet-4-20250514", "max_tokens": 1,

229 "messages": [{"role": "user", "content": "hi"}]},

230 timeout=10,

231 )

232 if resp.status_code == 200:

233 print("✅ 成功")

234 return True

235 elif resp.status_code == 401:

236 print("❌ Key 无效（401）")

237 return False

238 else:

239 print(f"⚠️ 返回 {resp.status_code}")

240 return True

241 except Exception as e:

242 print(f"⚠️ 连接异常: {e}")

243 return False

244 return False

245

246

247 def _save_config(provider_name: str, api_key: str):

248 """保存配置到 ~/.agentos/config.yaml。"""

249 CONFIG_DIR.mkdir(parents=True, exist_ok=True)

250 env_var = PROVIDERS[provider_name]["env_var"]

251

252 # Save .env file

253 env_content = f"# Nexus AgentOS — {PROVIDERS[provider_name]['label']} 配置\n"

254 env_content += f"{env_var}={api_key}\n\n"

255 env_content += "# 可选：配置多个 Provider 以实现自动回退\n"

256 env_content += "# OPENAI_API_KEY=sk-xxx\n"

257 env_content += "# DEEPSEEK_API_KEY=sk-xxx\n"

258 env_content += "# ANTHROPIC_API_KEY=sk-ant-xxx\n"

259 ENV_FILE.write_text(env_content)

260

261 # Save config.yaml

262 config = {

263 "version": "1.4.0",

264 "active_provider": provider_name,

265 "providers": {

266 provider_name: {

267 "env_var": env_var,

268 }

269 },

270 }

271 import yaml

272 with open(CONFIG_FILE, "w") as f:

273 yaml.dump(config, f, default_flow_style=False)

274

275 print(f"\n ✅ 配置已保存")

276 print(f" {CONFIG_FILE}")

277 print(f" {ENV_FILE}")

278

279

280 def _show_completion_message(provider_name: str):

281 """显示配置完成后引导。"""

282 from agentos import __version__

283 p = PROVIDERS[provider_name]

284

285 print()

286 print(" ╔══════════════════════════════════════════════╗")

287 print(" ║ ✅ 配置就绪！ ║")

288 print(" ╚══════════════════════════════════════════════╝")

289 print()

290 print(f" 当前已配置: {p['label']} ({p['default_model']})")

291 print()

292 print(" ── 快速开始 ──")

293 print()

294 print(f" # 运行任务")

295 print(f" agentos \"列出当前目录的文件\"")

296 print()

297 print(f" # 运行端到端示例")

298 print(f" python -m examples.multi_agent_research --topic \"量子计算\"")

299 print()

300 if provider_name != "openai":

301 print(f" # 指定使用 {p['label']}")

302 print(f" agentos --provider {provider_name} \"写一个 Python 爬虫\"")

303 print()

304 print(" ── 多 Provider 配置（可选） ──")

305 print()

306 print(f" 编辑 {ENV_FILE}，添加其他 API Key 即可实现自动回退:")

307 print(f" OPENAI_API_KEY=sk-xxx # 默认使用")

308 print(f" DEEPSEEK_API_KEY=sk-xxx # 回退 1")

309 print(f" ANTHROPIC_API_KEY=sk-ant-xxx # 回退 2")

310 print()

311 print(f" 重新运行 agentos init 修改配置。")

312 print(f" 或 agentos config-panel 打开浏览器版配置面板。")

313

314

315# ── 配置加载接口 ────────────────────────────────────────────

316

317

318 def load_config() -> dict:

319 """加载 ~/.agentos/config.yaml 和环境变量。

320

321 Returns:

322 dict: 包含 providers 和 active_provider 的配置字典。

323 """

324 config = {"providers": {}, "active_provider": None}

325

326 # 1. Load env vars

327 for name, info in PROVIDERS.items():

328 key = os.environ.get(info["env_var"])

329 if key:

330 config["providers"][name] = key

331

332 # 2. Load config file

333 if CONFIG_FILE.exists():

334 try:

335 import yaml

336 raw = yaml.safe_load(CONFIG_FILE.read_text())

337 if raw and "providers" in raw:

338 for name, pcfg in raw["providers"].items():

339 if name in PROVIDERS and name not in config["providers"]:

340 env_var = pcfg.get("env_var", PROVIDERS[name]["env_var"])

341 # Try to load from .env

342 if ENV_FILE.exists():

343 for line in ENV_FILE.read_text().splitlines():

344 if line.startswith(env_var + "="):

345 val = line.split("=", 1)[1].strip()

346 if val and val != "sk-xxx":

347 config["providers"][name] = val

348 break

349 if raw and "active_provider" in raw:

350 config["active_provider"] = raw["active_provider"]

351 except Exception:

352 pass

353

354 # 3. Determine active

355 for name in ["openai", "deepseek", "anthropic"]:

356 if config["providers"].get(name):

357 config["active_provider"] = config.get("active_provider") or name

358 break

359

360 return config

361

362

363 def config_status_text() -> str:

364 """返回一行配置状态文本，给 CLI help 用。"""

365 config = load_config()

366 if config["active_provider"]:

367 name = config["active_provider"]

368 label = PROVIDERS.get(name, {}).get("label", name)

369 return f"✅ {label} 已配置"

370 return "⬜ 未配置（运行 agentos init）"

371

372

373# ── CLI ────────────────────────────────────────────────────

374

375

376 def init_cli(args: list[str]):

377 """CLI 入口。"""

378 quick = "--quick" in args

379 reset = "--reset" in args

380

381 if reset:

382 if CONFIG_FILE.exists():

383 CONFIG_FILE.unlink()

384 if ENV_FILE.exists():

385 ENV_FILE.unlink()

386 print(" ✅ 配置已重置。运行 agentos init 重新配置。")

387 return

388

389 if quick:

390 # Quick mode: just create .env.example in current directory

391 example_path = Path.cwd() / ".env.example"

392 content = """# Nexus AgentOS 配置示例

393# 复制为 .env 并填入你的 API Key

394 OPENAI_API_KEY=sk-xxx

395 DEEPSEEK_API_KEY=sk-xxx

396 ANTHROPIC_API_KEY=sk-ant-xxx

397"""

398 example_path.write_text(content)

399 print(f" 已生成 {example_path}")

400 print(" 复制为 .env 并填入你的 API Key 即可使用。")

401 return

402

403 # Interactive mode

404 _print_banner()

405 current = _detect_current_config()

406 _print_status(current)

407

408 if current["configured_providers"]:

409 print(f" 检测到已有 API Key 配置。")

410 reconfig = input(" 是否重新配置？(y/N): ").strip().lower()

411 if reconfig not in ("y", "yes"):

412 _show_completion_message(current["active"] or current["configured_providers"][0])

413 return

414

415 provider = _select_provider()

416 api_key = _input_api_key(provider)

417 if api_key is None:

418 print(" 配置已取消。")

419 return

420

421 test_result = _test_connection(provider, api_key)

422 if test_result is False:

423 retry = input(" Key 验证失败，重试？(Y/n): ").strip().lower()

424 if retry not in ("", "y", "yes"):

425 print(" 配置已取消。")

426 return

427 # Try again recursively for simplicity

428 return init_cli(args)

429

430 _save_config(provider, api_key)

431 _show_completion_message(provider)

432

433

434# ── 项目脚手架（兼容旧接口） ──────────────────────────────────

435

436 TEMPLATES = {

437 "default": {

438 "agentos.yaml": """\

439# AgentOS v0.80 配置文件

440 version: "0.80.0"

441

442 models:

443 primary:

444 provider: openai

445 model_name: gpt-4o-mini

446 temperature: 0.7

447 max_tokens: 4096

448

449 loop:

450 max_iterations: 10

451 step_timeout: 120

452

453 observability:

454 tracer:

455 enabled: true

456 level: info

457""",

458 "main.py": """\

459\"""AgentOS — 我的 Agent 应用入口。\"""

460

461 from agentos import AgentLoop, LoopConfig

462

463

464 def main():

465 loop = AgentLoop(LoopConfig(max_iterations=5))

466 result = loop.run("你好，世界！")

467 print(result.output)

468

469

470 if __name__ == "__main__":

471 main()

472""",

473 ".env.example": """\

474# AgentOS 环境变量

475 OPENAI_API_KEY=sk-xxx

476 ANTHROPIC_API_KEY=sk-ant-xxx

477 GEMINI_API_KEY=AIza-xxx

478""",

479 },

480 "minimal": {

481 "agentos.yaml": """\

482 version: "0.80.0"

483 models:

484 primary:

485 provider: openai

486 model_name: gpt-4o-mini

487""",

488 "main.py": """\

489 from agentos import AgentLoop, LoopConfig

490

491 loop = AgentLoop(LoopConfig(max_iterations=3))

492 result = loop.run("你好，世界！")

493 print(result.output)

494""",

495 },

496}

497

498

499 def scaffold(project_dir: str, template: str = "default") -> list[str]:

500 """初始化 AgentOS 项目脚手架。

501

502 Args:

503 project_dir: 项目根目录路径。

504 template: 模板名称（"default" 或 "minimal"）。

505

506 Returns:

507 创建的文件路径列表。

508 """

509 files = TEMPLATES.get(template, TEMPLATES["default"])

510 project_path = Path(project_dir).resolve()

511 project_path.mkdir(parents=True, exist_ok=True)

512

513 created = []

514 for filename, content in files.items():

515 filepath = project_path / filename

516 if filepath.exists():

517 continue

518 with open(filepath, "w") as f:

519 f.write(content)

520 created.append(str(filepath))

521

522 return created

523

524

525 if __name__ == "__main__":

526 init_cli(sys.argv[1:])