1"""

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

3

4 启动: agentos config-panel

5 访问: http://localhost:18480

6"""

7

8 from __future__ import annotations

9

10 import http.server

11 import json

12 import os

13 import sys

14 import webbrowser

15 from pathlib import Path

16 from urllib.parse import urlparse, parse_qs

17

18

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

20 CONFIG_FILE = CONFIG_DIR / "config.yaml"

21 ENV_FILE = CONFIG_DIR / ".env"

22

23 CSS = """

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

25 body {

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

104 HTML = """<!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// 页面加载时检查当前配置

201 async 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

213 async 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

231 async 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

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

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

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

251 el.textContent = msg;

252}}

253

254 async 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

271 checkStatus();

272</script>

273</body>

274</html>

275"""

276

277 API_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

302 def _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

324 def _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

389 def _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

427 class 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

491 def 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

521 if __name__ == "__main__":

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

523 start_panel(port)