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
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""
2AgentOS 配置面板 — Web GUI,一键浏览器配置 API Key。
4启动: agentos config-panel
5访问: http://localhost:18480
6"""
8from __future__ import annotations
10import http.server
11import json
12import os
13import sys
14import webbrowser
15from pathlib import Path
16from urllib.parse import urlparse, parse_qs
19CONFIG_DIR = Path.home() / ".agentos"
20CONFIG_FILE = CONFIG_DIR / "config.yaml"
21ENV_FILE = CONFIG_DIR / ".env"
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"""
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>
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>
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>
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>
188 <!-- 整体状态 -->
189 <div id="global-status" class="status-bar">
190 未配置任何服务商。选择一个并填入 API Key 开始使用。
191 </div>
193 <div class="footer">
194 <p>API Key 安全保存在本地 <code>~/.agentos/</code> 目录中,不会上传到任何服务器。</p>
195 <p>配置完成后可在终端运行 <code>agentos "你的任务"</code> 开始使用。</p>
196 </div>
197</div>
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}}
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}}
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}}
248function setStatus(provider, cls, msg) {{
249 const el = document.getElementById('status-' + provider);
250 el.className = 'status status-' + cls;
251 el.textContent = msg;
252}}
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');
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}}
271checkStatus();
272</script>
273</body>
274</html>
275"""
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}
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
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)}"
389def _save_config(provider: str, api_key: str):
390 """保存配置到 ~/.agentos/。"""
391 CONFIG_DIR.mkdir(parents=True, exist_ok=True)
392 info = API_TEMPLATE[provider]
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")
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)
423 # Set env var for current process
424 os.environ[info["env_var"]] = api_key
427class PanelHandler(http.server.BaseHTTPRequestHandler):
428 """HTTP 请求处理。"""
430 def log_message(self, format, *args):
431 pass # 静默
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)
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)
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"))
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"))
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)
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}"
496 print(f"""
497 ╔══════════════════════════════════════════════╗
498 ║ AgentOS 配置面板已启动 ║
499 ╠══════════════════════════════════════════════╣
500 ║ ║
501 ║ 访问地址: {url} ║
502 ║ ║
503 ║ 按 Ctrl+C 停止服务 ║
504 ╚══════════════════════════════════════════════╝
505""")
507 if open_browser:
508 try:
509 webbrowser.open(url)
510 print(" 已自动打开浏览器。如未打开,请手动访问上方地址。\n")
511 except Exception:
512 print(" 请手动在浏览器中打开上方地址。\n")
514 try:
515 server.serve_forever()
516 except KeyboardInterrupt:
517 print("\n 配置面板已关闭。\n")
518 server.server_close()
521if __name__ == "__main__":
522 port = int(sys.argv[1]) if len(sys.argv) > 1 else 18480
523 start_panel(port)