Coverage for agentos/cli/config_panel.py: 17%

155 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-02 09:59 +0800

1""" 

2AgentOS 配置面板 — Web GUI,一键浏览器配置 API Key。 

3 

4启动: agentos config-panel 

5访问: http://localhost:18480 

6""" 

7 

8from __future__ import annotations 

9 

10import http.server 

11import json 

12import os 

13import sys 

14import webbrowser 

15from pathlib import Path 

16from urllib.parse import urlparse, parse_qs 

17 

18 

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

20CONFIG_FILE = CONFIG_DIR / "config.yaml" 

21ENV_FILE = CONFIG_DIR / ".env" 

22 

23CSS = """ 

24* { margin: 0; padding: 0; box-sizing: border-box; } 

25body { 

26 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", 

27 "Microsoft YaHei", "Helvetica Neue", sans-serif; 

28 background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); 

29 min-height: 100vh; color: #e0e0e0; padding: 24px; 

30} 

31.container { max-width: 720px; margin: 0 auto; } 

32.header { 

33 text-align: center; padding: 40px 0 32px; 

34} 

35.header h1 { 

36 font-size: 28px; font-weight: 700; 

37 background: linear-gradient(90deg, #667eea, #764ba2); 

38 -webkit-background-clip: text; -webkit-text-fill-color: transparent; 

39} 

40.header p { color: #a0a0b8; margin-top: 8px; font-size: 14px; } 

41.card { 

42 background: rgba(255,255,255,0.06); 

43 backdrop-filter: blur(12px); 

44 border: 1px solid rgba(255,255,255,0.10); 

45 border-radius: 16px; padding: 28px; margin-bottom: 20px; 

46 transition: border-color 0.2s; 

47} 

48.card:hover { border-color: rgba(102,126,234,0.4); } 

49.card h3 { 

50 font-size: 17px; font-weight: 600; margin-bottom: 6px; 

51 display: flex; align-items: center; gap: 10px; 

52} 

53.card h3 .badge { 

54 font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 10px; 

55} 

56.badge-recommend { background: #667eea22; color: #667eea; } 

57.badge cheap { background: #4caf5022; color: #4caf50; } 

58.badge strong { background: #ff980022; color: #ff9800; } 

59.card .desc { color: #9090a8; font-size: 13px; margin-bottom: 14px; line-height: 1.6; } 

60.card .info-row { 

61 display: flex; gap: 20px; margin-bottom: 14px; flex-wrap: wrap; 

62} 

63.card .info-item { 

64 font-size: 12px; color: #808098; 

65} 

66.card .info-item strong { color: #c0c0d0; font-weight: 600; } 

67.input-row { display: flex; gap: 10px; } 

68.input-row input { 

69 flex: 1; padding: 10px 14px; 

70 background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.12); 

71 border-radius: 10px; color: #e0e0e0; font-size: 14px; 

72 outline: none; transition: border-color 0.2s; 

73} 

74.input-row input:focus { border-color: #667eea; } 

75.input-row input::placeholder { color: #555; } 

76.btn { 

77 padding: 10px 20px; border-radius: 10px; border: none; 

78 font-size: 14px; font-weight: 600; cursor: pointer; 

79 transition: all 0.2s; 

80} 

81.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; } 

82.btn-primary:hover { filter: brightness(1.1); transform: translateY(-1px); } 

83.btn-outline { 

84 background: transparent; border: 1px solid rgba(255,255,255,0.15); 

85 color: #c0c0d8; 

86} 

87.btn-outline:hover { background: rgba(255,255,255,0.06); } 

88.btn-success { background: #4caf50; color: #fff; } 

89.status { font-size: 12px; margin-top: 8px; height: 20px; } 

90.status-ok { color: #4caf50; } 

91.status-err { color: #f44336; } 

92.status-checking { color: #ff9800; } 

93.status-bar { 

94 text-align: center; margin-top: 24px; padding: 16px; 

95 background: rgba(76,175,80,0.10); border-radius: 12px; 

96 font-size: 14px; color: #81c784; 

97} 

98.status-bar.warning { 

99 background: rgba(255,152,0,0.10); color: #ffb74d; 

100} 

101.footer { text-align: center; padding: 24px; color: #606078; font-size: 12px; } 

102""" 

103 

104HTML = """<!DOCTYPE html> 

105<html lang="zh-CN"> 

106<head> 

107<meta charset="utf-8"> 

108<meta name="viewport" content="width=device-width, initial-scale=1"> 

109<title>AgentOS 配置面板</title> 

110<style>{css}</style> 

111</head> 

112<body> 

113<div class="container"> 

114 <div class="header"> 

115 <h1>🔧 AgentOS 配置面板</h1> 

116 <p>选择一个 AI 服务商,填入 API Key,30 秒完成配置</p> 

117 </div> 

118 

119 <!-- OpenAI --> 

120 <div class="card" id="card-openai"> 

121 <h3> 

122 <span>🔵 OpenAI</span> 

123 <span class="badge badge-recommend">推荐</span> 

124 </h3> 

125 <div class="desc"> 

126 最成熟的 AI 服务商,提供 GPT-4o、GPT-4o-mini 等模型。 

127 适合日常对话、代码生成、文档处理,中英文均出色。响应快、稳定性高。 

128 </div> 

129 <div class="info-row"> 

130 <div class="info-item">默认模型: <strong>gpt-4o-mini</strong></div> 

131 <div class="info-item">费用: <strong>低 ~ 中(约 $0.15/百万token)</strong></div> 

132 <div class="info-item">注册: <a href="https://platform.openai.com/api-keys" target="_blank" style="color:#667eea">platform.openai.com</a></div> 

133 </div> 

134 <div class="input-row"> 

135 <input type="password" id="key-openai" placeholder="粘贴你的 OpenAI API Key(以 sk- 开头)"> 

136 <button class="btn btn-primary" onclick="save('openai')">保存并验证</button> 

137 <button class="btn btn-outline" onclick="verify('openai')">仅验证</button> 

138 </div> 

139 <div class="status" id="status-openai"></div> 

140 </div> 

141 

142 <!-- DeepSeek --> 

143 <div class="card" id="card-deepseek"> 

144 <h3> 

145 <span>🟢 DeepSeek</span> 

146 <span class="badge cheap">最实惠</span> 

147 </h3> 

148 <div class="desc"> 

149 国产高性价比 AI 服务商,deepseek-chat 模型在中文理解和代码生成方面表现突出。 

150 价格极低(约为 OpenAI 的 1/10),适合高频调用和预算敏感的场景。 

151 </div> 

152 <div class="info-row"> 

153 <div class="info-item">默认模型: <strong>deepseek-chat</strong></div> 

154 <div class="info-item">费用: <strong>极低(约 ¥1/百万token)</strong></div> 

155 <div class="info-item">注册: <a href="https://platform.deepseek.com/api_keys" target="_blank" style="color:#667eea">platform.deepseek.com</a></div> 

156 </div> 

157 <div class="input-row"> 

158 <input type="password" id="key-deepseek" placeholder="粘贴你的 DeepSeek API Key(以 sk- 开头)"> 

159 <button class="btn btn-primary" onclick="save('deepseek')">保存并验证</button> 

160 <button class="btn btn-outline" onclick="verify('deepseek')">仅验证</button> 

161 </div> 

162 <div class="status" id="status-deepseek"></div> 

163 </div> 

164 

165 <!-- Anthropic --> 

166 <div class="card" id="card-anthropic"> 

167 <h3> 

168 <span>🟣 Anthropic (Claude)</span> 

169 <span class="badge strong">最强推理</span> 

170 </h3> 

171 <div class="desc"> 

172 Claude 系列擅长深度推理、长篇分析和安全对齐,创意写作和复杂逻辑任务表现顶级。 

173 适合需要深度思考的场景,如研究分析、合规审查、长文生成。 

174 </div> 

175 <div class="info-row"> 

176 <div class="info-item">默认模型: <strong>claude-sonnet-4</strong></div> 

177 <div class="info-item">费用: <strong>中 ~ 高(约 $3/百万token)</strong></div> 

178 <div class="info-item">注册: <a href="https://console.anthropic.com/keys" target="_blank" style="color:#667eea">console.anthropic.com</a></div> 

179 </div> 

180 <div class="input-row"> 

181 <input type="password" id="key-anthropic" placeholder="粘贴你的 Anthropic API Key(以 sk-ant- 开头)"> 

182 <button class="btn btn-primary" onclick="save('anthropic')">保存并验证</button> 

183 <button class="btn btn-outline" onclick="verify('anthropic')">仅验证</button> 

184 </div> 

185 <div class="status" id="status-anthropic"></div> 

186 </div> 

187 

188 <!-- 整体状态 --> 

189 <div id="global-status" class="status-bar"> 

190 未配置任何服务商。选择一个并填入 API Key 开始使用。 

191 </div> 

192 

193 <div class="footer"> 

194 <p>API Key 安全保存在本地 <code>~/.agentos/</code> 目录中,不会上传到任何服务器。</p> 

195 <p>配置完成后可在终端运行 <code>agentos "你的任务"</code> 开始使用。</p> 

196 </div> 

197</div> 

198 

199<script> 

200// 页面加载时检查当前配置 

201async function checkStatus() {{ 

202 const resp = await fetch('/api/status'); 

203 const data = await resp.json(); 

204 for (const p of ['openai', 'deepseek', 'anthropic']) {{ 

205 if (data[p] && data[p].configured) {{ 

206 setStatus(p, 'ok', '已配置 ✅'); 

207 document.getElementById('key-' + p).value = data[p].preview || ''; 

208 }} 

209 }} 

210 updateGlobalStatus(data); 

211}} 

212 

213async function save(provider) {{ 

214 const key = document.getElementById('key-' + provider).value.trim(); 

215 if (!key) {{ setStatus(provider, 'err', '请输入 API Key'); return; }} 

216 setStatus(provider, 'checking', '正在验证...'); 

217 const resp = await fetch('/api/save', {{ 

218 method: 'POST', 

219 headers: {{'Content-Type': 'application/json'}}, 

220 body: JSON.stringify({{provider, key}}) 

221 }}); 

222 const data = await resp.json(); 

223 if (data.ok) {{ 

224 setStatus(provider, 'ok', '配置成功 ✅ — ' + (data.model || '')); 

225 }} else {{ 

226 setStatus(provider, 'err', '失败: ' + (data.error || '未知错误')); 

227 }} 

228 updateGlobalStatus(data); 

229}} 

230 

231async function verify(provider) {{ 

232 const key = document.getElementById('key-' + provider).value.trim(); 

233 if (!key) {{ setStatus(provider, 'err', '请先输入 API Key'); return; }} 

234 setStatus(provider, 'checking', '正在验证...'); 

235 const resp = await fetch('/api/verify', {{ 

236 method: 'POST', 

237 headers: {{'Content-Type': 'application/json'}}, 

238 body: JSON.stringify({{provider, key}}) 

239 }}); 

240 const data = await resp.json(); 

241 if (data.valid) {{ 

242 setStatus(provider, 'ok', 'Key 有效 ✅'); 

243 }} else {{ 

244 setStatus(provider, 'err', 'Key 无效: ' + (data.error || '')); 

245 }} 

246}} 

247 

248function setStatus(provider, cls, msg) {{ 

249 const el = document.getElementById('status-' + provider); 

250 el.className = 'status status-' + cls; 

251 el.textContent = msg; 

252}} 

253 

254async function updateGlobalStatus(data) {{ 

255 const el = document.getElementById('global-status'); 

256 const configured = []; 

257 if (data.openai && data.openai.configured) configured.push('OpenAI'); 

258 if (data.deepseek && data.deepseek.configured) configured.push('DeepSeek'); 

259 if (data.anthropic && data.anthropic.configured) configured.push('Anthropic'); 

260 

261 if (configured.length > 0) {{ 

262 el.className = 'status-bar'; 

263 el.innerHTML = '✅ 已配置: ' + configured.join('、') + 

264 ' 。在终端运行 <code>agentos "你的任务"</code> 开始使用。'; 

265 }} else {{ 

266 el.className = 'status-bar warning'; 

267 el.textContent = '未配置任何服务商。选择一个并填入 API Key 完成配置。'; 

268 }} 

269}} 

270 

271checkStatus(); 

272</script> 

273</body> 

274</html> 

275""" 

276 

277API_TEMPLATE = { 

278 "openai": { 

279 "env_var": "OPENAI_API_KEY", 

280 "label": "OpenAI", 

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

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

283 "key_prefix": "sk-", 

284 }, 

285 "deepseek": { 

286 "env_var": "DEEPSEEK_API_KEY", 

287 "label": "DeepSeek", 

288 "default_model": "deepseek-chat", 

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

290 "key_prefix": "sk-", 

291 }, 

292 "anthropic": { 

293 "env_var": "ANTHROPIC_API_KEY", 

294 "label": "Anthropic", 

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

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

297 "key_prefix": "sk-ant-", 

298 }, 

299} 

300 

301 

302def _get_status_dict() -> dict: 

303 """获取所有 Provider 的配置状态。""" 

304 status = {} 

305 for name, info in API_TEMPLATE.items(): 

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

307 if not key and ENV_FILE.exists(): 

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

309 if line.startswith(info["env_var"] + "="): 

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

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

312 key = val 

313 break 

314 preview = "" 

315 if key: 

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

317 status[name] = { 

318 "configured": bool(key), 

319 "preview": preview, 

320 } 

321 return status 

322 

323 

324def _test_connection(provider: str, api_key: str) -> tuple[bool, str]: 

325 """测试 API 连接。""" 

326 try: 

327 import httpx 

328 if provider == "openai": 

329 resp = httpx.get( 

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

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

332 timeout=10, 

333 ) 

334 if resp.status_code == 200: 

335 return True, "" 

336 elif resp.status_code == 401: 

337 return False, "API Key 无效(401 未授权)" 

338 elif resp.status_code == 429: 

339 return False, "请求过于频繁,请稍后重试" 

340 else: 

341 return False, f"返回状态码 {resp.status_code},请检查 Key 是否有对应权限" 

342 elif provider == "deepseek": 

343 resp = httpx.post( 

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

345 headers={ 

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

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

348 }, 

349 json={ 

350 "model": "deepseek-chat", 

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

352 "max_tokens": 1, 

353 }, 

354 timeout=10, 

355 ) 

356 if resp.status_code == 200: 

357 return True, "" 

358 elif resp.status_code == 401: 

359 return False, "API Key 无效(401 未授权)" 

360 elif resp.status_code == 402: 

361 return False, "账户余额不足,请充值" 

362 else: 

363 return False, f"返回状态码 {resp.status_code},请检查" 

364 elif provider == "anthropic": 

365 resp = httpx.post( 

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

367 headers={ 

368 "x-api-key": api_key, 

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

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

371 }, 

372 json={ 

373 "model": "claude-sonnet-4-20250514", 

374 "max_tokens": 1, 

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

376 }, 

377 timeout=10, 

378 ) 

379 if resp.status_code == 200: 

380 return True, "" 

381 elif resp.status_code == 401: 

382 return False, "API Key 无效(401 未授权)" 

383 else: 

384 return False, f"返回状态码 {resp.status_code},请检查" 

385 except Exception as e: 

386 return False, f"网络连接失败: {str(e)}" 

387 

388 

389def _save_config(provider: str, api_key: str): 

390 """保存配置到 ~/.agentos/。""" 

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

392 info = API_TEMPLATE[provider] 

393 

394 # Update .env 

395 env_lines = [] 

396 if ENV_FILE.exists(): 

397 env_lines = ENV_FILE.read_text().splitlines() 

398 found = False 

399 new_env = [] 

400 for line in env_lines: 

401 if line.startswith(info["env_var"] + "="): 

402 new_env.append(f"{info['env_var']}={api_key}") 

403 found = True 

404 else: 

405 new_env.append(line) 

406 if not found: 

407 new_env.append(f"{info['env_var']}={api_key}") 

408 ENV_FILE.write_text("\n".join(new_env) + "\n") 

409 

410 # Update config.yaml 

411 config = {"version": "1.4.1", "active_provider": provider} 

412 import yaml 

413 if CONFIG_FILE.exists(): 

414 existing = yaml.safe_load(CONFIG_FILE.read_text()) 

415 if existing and "providers" in existing: 

416 config["providers"] = existing["providers"] 

417 if "providers" not in config: 

418 config["providers"] = {} 

419 config["providers"][provider] = {"env_var": info["env_var"]} 

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

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

422 

423 # Set env var for current process 

424 os.environ[info["env_var"]] = api_key 

425 

426 

427class PanelHandler(http.server.BaseHTTPRequestHandler): 

428 """HTTP 请求处理。""" 

429 

430 def log_message(self, format, *args): 

431 pass # 静默 

432 

433 def do_GET(self): 

434 if self.path == "/" or self.path == "/index.html": 

435 html = HTML.format(css=CSS) 

436 self._send_html(html) 

437 elif self.path == "/api/status": 

438 status = _get_status_dict() 

439 self._send_json(status) 

440 else: 

441 self.send_error(404) 

442 

443 def do_POST(self): 

444 if self.path == "/api/save": 

445 body = self._read_body() 

446 provider = body.get("provider", "") 

447 key = body.get("key", "") 

448 if provider not in API_TEMPLATE: 

449 self._send_json({"ok": False, "error": "未知服务商"}) 

450 return 

451 ok, err = _test_connection(provider, key) 

452 if ok: 

453 _save_config(provider, key) 

454 info = API_TEMPLATE[provider] 

455 self._send_json({ 

456 "ok": True, 

457 "model": info["default_model"], 

458 }) 

459 else: 

460 self._send_json({"ok": False, "error": err}) 

461 elif self.path == "/api/verify": 

462 body = self._read_body() 

463 provider = body.get("provider", "") 

464 key = body.get("key", "") 

465 if provider not in API_TEMPLATE: 

466 self._send_json({"valid": False, "error": "未知服务商"}) 

467 return 

468 ok, err = _test_connection(provider, key) 

469 self._send_json({"valid": ok, "error": err}) 

470 else: 

471 self.send_error(404) 

472 

473 def _send_html(self, html: str): 

474 self.send_response(200) 

475 self.send_header("Content-Type", "text/html; charset=utf-8") 

476 self.end_headers() 

477 self.wfile.write(html.encode("utf-8")) 

478 

479 def _send_json(self, data: dict): 

480 self.send_response(200) 

481 self.send_header("Content-Type", "application/json; charset=utf-8") 

482 self.end_headers() 

483 self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8")) 

484 

485 def _read_body(self) -> dict: 

486 length = int(self.headers.get("Content-Length", 0)) 

487 raw = self.rfile.read(length).decode("utf-8") if length else "{}" 

488 return json.loads(raw) 

489 

490 

491def start_panel(port: int = 18480, open_browser: bool = True): 

492 """启动配置面板 HTTP 服务。""" 

493 server = http.server.HTTPServer(("127.0.0.1", port), PanelHandler) 

494 url = f"http://127.0.0.1:{port}" 

495 

496 print(f""" 

497 ╔══════════════════════════════════════════════╗ 

498 ║ AgentOS 配置面板已启动 ║ 

499 ╠══════════════════════════════════════════════╣ 

500 ║ ║ 

501 ║ 访问地址: {url} ║ 

502 ║ ║ 

503 ║ 按 Ctrl+C 停止服务 ║ 

504 ╚══════════════════════════════════════════════╝ 

505""") 

506 

507 if open_browser: 

508 try: 

509 webbrowser.open(url) 

510 print(" 已自动打开浏览器。如未打开,请手动访问上方地址。\n") 

511 except Exception: 

512 print(" 请手动在浏览器中打开上方地址。\n") 

513 

514 try: 

515 server.serve_forever() 

516 except KeyboardInterrupt: 

517 print("\n 配置面板已关闭。\n") 

518 server.server_close() 

519 

520 

521if __name__ == "__main__": 

522 port = int(sys.argv[1]) if len(sys.argv) > 1 else 18480 

523 start_panel(port)