1. 清除旧版 "agent" 废弃键 — 保存 channel 配置时自动删除旧的 agent 键, 防止 OpenClaw 报 "agent.* was moved" 错误(前端 + 服务端双重防御) 2. Dashboard URL 动态端口探测 — 将硬编码 18789 改为扫描 18789-18799, 解决端口被占用时打不开 Dashboard 的问题 3. 安装脚本配置格式升级 — install.sh / install.ps1 / install-silent.ps1 生成的 openclaw.json 从旧的 "agent" 格式迁移到 "agents.defaults.model.primary" 4. 文档示例同步更新 — CLAUDE.md / cloud.html / guide.html 中的配置示例 影响文件: 14 个(portable/, install/, website/, u-claw-app/, usb-release/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
747 lines
35 KiB
HTML
747 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>U-Claw 配置中心</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
|
||
.container { max-width: 720px; margin: 0 auto; padding: 28px 20px 60px; }
|
||
h1 { text-align: center; font-size: 1.8em; margin-bottom: 4px; }
|
||
h1 .lobster { color: #ff6b35; }
|
||
.subtitle { text-align: center; color: #888; margin-bottom: 24px; font-size: 0.92em; }
|
||
|
||
/* Steps */
|
||
.steps { display: flex; gap: 0; margin-bottom: 24px; background: #1a1a1a; border-radius: 10px; overflow: hidden; border: 1px solid #333; }
|
||
.step { flex: 1; text-align: center; padding: 12px 8px; font-size: 0.85em; color: #666; cursor: pointer; transition: all 0.2s; }
|
||
.step.active { color: #ff6b35; background: rgba(255,107,53,0.08); font-weight: 600; }
|
||
.step.done { color: #4caf50; }
|
||
.step .num { display: inline-block; width: 22px; height: 22px; line-height: 22px; border-radius: 50%; background: #333; color: #888; font-size: 0.8em; margin-right: 4px; }
|
||
.step.active .num { background: #ff6b35; color: #fff; }
|
||
.step.done .num { background: #4caf50; color: #fff; }
|
||
|
||
/* Section */
|
||
.section { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 16px; display: none; }
|
||
.section.active { display: block; }
|
||
.section h2 { color: #ff6b35; font-size: 1.15em; margin-bottom: 6px; }
|
||
.section .desc { color: #888; font-size: 0.88em; margin-bottom: 16px; }
|
||
|
||
/* Model cards */
|
||
.model-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
|
||
.model-card { background: #222; border: 2px solid #333; border-radius: 10px; padding: 14px; cursor: pointer; transition: all 0.15s; position: relative; }
|
||
.model-card:hover { border-color: #555; transform: translateY(-1px); }
|
||
.model-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
|
||
.model-card h4 { color: #fff; font-size: 0.95em; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.model-card p { color: #888; font-size: 0.78em; line-height: 1.4; }
|
||
.tag { font-size: 0.68em; padding: 2px 7px; border-radius: 10px; font-weight: 500; }
|
||
.tag.hot { background: rgba(255,107,53,0.2); color: #ff6b35; }
|
||
.tag.free { background: rgba(76,175,80,0.2); color: #4caf50; }
|
||
.tag.cn { background: rgba(33,150,243,0.2); color: #2196f3; }
|
||
.tag.fast { background: rgba(156,39,176,0.2); color: #ce93d8; }
|
||
.tag.cheap { background: rgba(255,193,7,0.2); color: #ffd54f; }
|
||
.check { position: absolute; top: 8px; right: 10px; color: #ff6b35; font-size: 1.2em; display: none; }
|
||
.model-card.selected .check { display: block; }
|
||
|
||
/* Buy link */
|
||
.buy-link { display: inline-flex; align-items: center; gap: 4px; color: #ff6b35; text-decoration: none; font-size: 0.82em; margin-top: 6px; }
|
||
.buy-link:hover { text-decoration: underline; }
|
||
|
||
/* Forms */
|
||
.form-group { margin-bottom: 16px; }
|
||
label { display: block; color: #ccc; margin-bottom: 6px; font-size: 0.9em; }
|
||
input[type="text"], input[type="password"] {
|
||
width: 100%; padding: 11px 14px; background: #222; border: 1px solid #444; border-radius: 8px;
|
||
color: #fff; font-size: 0.95em; outline: none; transition: border-color 0.2s;
|
||
}
|
||
input:focus { border-color: #ff6b35; }
|
||
.input-wrap { position: relative; }
|
||
.input-wrap input { padding-right: 44px; }
|
||
.toggle-pw { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #888; cursor: pointer; font-size: 0.85em; }
|
||
.toggle-pw:hover { color: #ccc; }
|
||
.hint { color: #888; font-size: 0.8em; margin-top: 6px; line-height: 1.5; }
|
||
.hint a { color: #ff6b35; text-decoration: none; }
|
||
.hint a:hover { text-decoration: underline; }
|
||
|
||
/* API Key info box */
|
||
.api-info { background: #1e1e2e; border: 1px solid #333; border-radius: 8px; padding: 14px; margin-bottom: 16px; }
|
||
.api-info h4 { color: #ff6b35; font-size: 0.9em; margin-bottom: 8px; }
|
||
.api-info .provider-links { display: flex; flex-direction: column; gap: 6px; }
|
||
.api-info a { color: #82aaff; text-decoration: none; font-size: 0.85em; display: flex; align-items: center; gap: 6px; }
|
||
.api-info a:hover { color: #ff6b35; }
|
||
|
||
/* Buttons */
|
||
.btn { display: inline-block; padding: 12px 28px; border: none; border-radius: 8px; font-size: 0.95em; cursor: pointer; transition: all 0.15s; font-weight: 500; }
|
||
.btn-primary { background: #ff6b35; color: #fff; }
|
||
.btn-primary:hover { background: #e55a25; }
|
||
.btn-primary:disabled { background: #444; color: #888; cursor: not-allowed; }
|
||
.btn-secondary { background: #333; color: #ccc; }
|
||
.btn-secondary:hover { background: #444; }
|
||
.btn-row { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
|
||
|
||
/* Result */
|
||
.result { text-align: center; padding: 40px 20px; }
|
||
.result .icon { font-size: 3em; margin-bottom: 12px; }
|
||
.result h2 { color: #4caf50; margin-bottom: 8px; }
|
||
.result p { color: #888; font-size: 0.92em; line-height: 1.6; }
|
||
.result .url { color: #ff6b35; font-size: 1.1em; font-weight: 600; margin: 12px 0; }
|
||
|
||
/* Toast */
|
||
.toast { position: fixed; top: 20px; right: 20px; background: #4caf50; color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 0.9em; opacity: 0; transform: translateY(-10px); transition: all 0.3s; z-index: 999; }
|
||
.toast.show { opacity: 1; transform: translateY(0); }
|
||
.toast.error { background: #f44336; }
|
||
|
||
/* Channel form */
|
||
.channel-card { transition: all 0.2s; }
|
||
.channel-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
|
||
.channel-form { padding-top: 0; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
|
||
.channel-form.open { max-height: 300px; padding-top: 12px; }
|
||
.channel-form .form-group { margin-bottom: 10px; }
|
||
.channel-form label { font-size: 0.82em; color: #aaa; margin-bottom: 4px; }
|
||
.channel-form input { font-size: 0.88em; padding: 9px 12px; }
|
||
.channel-form .hint { font-size: 0.75em; margin-top: 4px; }
|
||
|
||
/* Status bar */
|
||
.status-bar {
|
||
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
||
padding: 10px 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||
.dot.on { background: #4caf50; animation: pulse 2s infinite; }
|
||
.dot.off { background: #f44336; }
|
||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||
.status-text { flex: 1; font-size: 0.9em; }
|
||
.status-text a { color: #ff6b35; text-decoration: none; }
|
||
|
||
/* Skills section */
|
||
.skills-section { margin-top: 8px; }
|
||
.skill-list { display: flex; flex-direction: column; gap: 8px; }
|
||
.skill-item { background: #222; border: 1px solid #333; border-radius: 8px; padding: 12px; display: flex; align-items: center; justify-content: space-between; }
|
||
.skill-item .info h4 { color: #fff; font-size: 0.9em; }
|
||
.skill-item .info p { color: #888; font-size: 0.78em; }
|
||
.skill-item .status { font-size: 0.8em; color: #4caf50; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1><span class="lobster">🦞</span> U-Claw 配置中心</h1>
|
||
<p class="subtitle">选择模型,填入 API Key,一键启动</p>
|
||
|
||
<!-- Connection status -->
|
||
<div class="status-bar">
|
||
<div class="dot" id="statusDot"></div>
|
||
<div class="status-text" id="statusText">检测中...</div>
|
||
</div>
|
||
|
||
<!-- Steps -->
|
||
<div class="steps">
|
||
<div class="step active" data-step="1" onclick="goStep(1)"><span class="num">1</span>选模型</div>
|
||
<div class="step" data-step="2" onclick="goStep(2)"><span class="num">2</span>填 Key</div>
|
||
<div class="step" data-step="3" onclick="goStep(3)"><span class="num">3</span>启动</div>
|
||
</div>
|
||
|
||
<!-- Step 1: Select Model -->
|
||
<div class="section active" id="step1">
|
||
<h2>选择 AI 模型</h2>
|
||
<p class="desc">推荐国内用户用 MiniMax / Kimi / DeepSeek</p>
|
||
|
||
<div class="model-grid">
|
||
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-Text-01">
|
||
<span class="check">✓</span>
|
||
<h4>MiniMax <span class="tag hot">推荐</span><span class="tag cn">国内</span></h4>
|
||
<p>MiniMax-Text-01,速度快,性价比高</p>
|
||
<a class="buy-link" href="https://platform.minimaxi.com/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="kimi" data-base="https://api.moonshot.cn/v1" data-model="moonshot-v1-auto">
|
||
<span class="check">✓</span>
|
||
<h4>Kimi (月之暗面) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||
<p>moonshot-v1-auto,智能选模型</p>
|
||
<a class="buy-link" href="https://platform.moonshot.cn/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
|
||
<span class="check">✓</span>
|
||
<h4>DeepSeek <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
||
<p>deepseek-chat,超低价格</p>
|
||
<a class="buy-link" href="https://platform.deepseek.com/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="zai" data-base="" data-model="glm-5">
|
||
<span class="check">✓</span>
|
||
<h4>智谱 GLM <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||
<p>glm-5(zai provider),免费额度可用</p>
|
||
<a class="buy-link" href="https://open.bigmodel.cn/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="qwen" data-base="https://dashscope.aliyuncs.com/compatible-mode/v1" data-model="qwen-turbo">
|
||
<span class="check">✓</span>
|
||
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||
<p>qwen-turbo,阿里云出品</p>
|
||
<a class="buy-link" href="https://dashscope.console.aliyun.com/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="doubao" data-base="https://ark.cn-beijing.volces.com/api/v3" data-model="doubao-1.5-pro-32k">
|
||
<span class="check">✓</span>
|
||
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||
<p>doubao-1.5-pro,字节跳动</p>
|
||
<a class="buy-link" href="https://console.volcengine.com/ark" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="openai" data-base="https://api.openai.com/v1" data-model="gpt-4o">
|
||
<span class="check">✓</span>
|
||
<h4>OpenAI <span class="tag hot">强</span></h4>
|
||
<p>GPT-4o,需翻墙或中转</p>
|
||
<a class="buy-link" href="https://platform.openai.com/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="anthropic" data-base="https://api.anthropic.com/v1" data-model="claude-sonnet-4-20250514">
|
||
<span class="check">✓</span>
|
||
<h4>Claude <span class="tag hot">强</span></h4>
|
||
<p>Claude Sonnet 4,需翻墙或中转</p>
|
||
<a class="buy-link" href="https://console.anthropic.com/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="groq" data-base="https://api.groq.com/openai/v1" data-model="llama-3.3-70b-versatile">
|
||
<span class="check">✓</span>
|
||
<h4>Groq <span class="tag fast">极快</span><span class="tag free">有免费</span></h4>
|
||
<p>Llama 3.3 70B,超高速推理</p>
|
||
<a class="buy-link" href="https://console.groq.com/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="siliconflow" data-base="https://api.siliconflow.cn/v1" data-model="Qwen/Qwen2.5-72B-Instruct">
|
||
<span class="check">✓</span>
|
||
<h4>硅基流动 <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
||
<p>多模型聚合,价格低</p>
|
||
<a class="buy-link" href="https://cloud.siliconflow.cn/" target="_blank">→ 获取 API Key</a>
|
||
</div>
|
||
<div class="model-card" data-provider="custom" data-base="" data-model="" style="grid-column: 1 / -1; border-style: dashed;">
|
||
<span class="check">✓</span>
|
||
<h4>🔧 自定义 API <span class="tag">OpenAI 兼容</span></h4>
|
||
<p>填写任意 OpenAI 兼容 API 地址(OpenRouter、API2D、New API、One API 等)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" id="nextStep1" disabled>下一步 →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: API Key -->
|
||
<div class="section" id="step2">
|
||
<h2>填写 API Key</h2>
|
||
<p class="desc" id="step2desc">请填写对应模型的 API Key</p>
|
||
|
||
<div class="api-info" id="buyInfo">
|
||
<h4>📌 如何获取 API Key?</h4>
|
||
<div class="provider-links" id="providerLinks"></div>
|
||
</div>
|
||
|
||
<div id="customFields" style="display:none;">
|
||
<div class="form-group">
|
||
<label>API 地址 (Base URL)</label>
|
||
<input type="text" id="customBase" placeholder="https://api.example.com/v1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>模型名称</label>
|
||
<input type="text" id="customModel" placeholder="gpt-4o">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>API Key</label>
|
||
<div class="input-wrap">
|
||
<input type="password" id="apiKey" placeholder="sk-...">
|
||
<button class="toggle-pw" onclick="togglePw('apiKey')">显示</button>
|
||
</div>
|
||
<p class="hint">你的 Key 仅保存在本地 U 盘,不会上传到任何服务器</p>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn btn-secondary" onclick="goStep(1)">← 返回</button>
|
||
<button class="btn btn-primary" onclick="saveConfig()">保存并继续 →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Done -->
|
||
<div class="section" id="step3">
|
||
<div class="result">
|
||
<div class="icon">🦞</div>
|
||
<h2>配置完成!</h2>
|
||
<p>模型和 API Key 已保存到本地</p>
|
||
<div class="url" id="gatewayUrl">http://127.0.0.1:18789</div>
|
||
|
||
<div style="margin-top: 24px; text-align: left;">
|
||
<h3 style="color: #ff6b35; font-size: 0.95em; margin-bottom: 12px;">📱 接入聊天平台(可选)</h3>
|
||
<p style="color: #888; font-size: 0.82em; margin-bottom: 12px;">点击卡片展开填写,不填可直接跳过:</p>
|
||
<div class="model-grid">
|
||
<div class="model-card channel-card" onclick="toggleChannel(this, 'telegram')">
|
||
<h4>✈️ Telegram</h4>
|
||
<p>接入 Telegram Bot</p>
|
||
<div class="channel-form" id="form-telegram" onclick="event.stopPropagation()">
|
||
<div class="form-group">
|
||
<label>Bot Token</label>
|
||
<input type="text" id="ch-telegram-token" placeholder="123456:ABC-DEF...">
|
||
<p class="hint"><a href="https://t.me/BotFather" target="_blank">→ 从 @BotFather 获取</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="model-card channel-card" onclick="toggleChannel(this, 'qqbot')">
|
||
<h4>🐧 QQ Bot</h4>
|
||
<p>接入 QQ 群/私聊机器人</p>
|
||
<div class="channel-form" id="form-qqbot" onclick="event.stopPropagation()">
|
||
<div class="form-group">
|
||
<label>App ID</label>
|
||
<input type="text" id="ch-qqbot-appid" placeholder="应用 ID">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Client Secret</label>
|
||
<input type="text" id="ch-qqbot-secret" placeholder="应用密钥">
|
||
<p class="hint"><a href="https://q.qq.com" target="_blank">→ 从 QQ 开放平台获取</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="model-card channel-card" onclick="toggleChannel(this, 'feishu')">
|
||
<h4>📘 飞书</h4>
|
||
<p>接入飞书机器人</p>
|
||
<div class="channel-form" id="form-feishu" onclick="event.stopPropagation()">
|
||
<div class="form-group">
|
||
<label>App ID</label>
|
||
<input type="text" id="ch-feishu-appid" placeholder="cli_xxx...">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>App Secret</label>
|
||
<input type="text" id="ch-feishu-secret" placeholder="应用密钥">
|
||
<p class="hint"><a href="https://open.feishu.cn/app" target="_blank">→ 从飞书开放平台获取</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="model-card channel-card" onclick="toggleChannel(this, 'wecom')">
|
||
<h4>🏢 企业微信</h4>
|
||
<p>接入企业微信机器人</p>
|
||
<div class="channel-form" id="form-wecom" onclick="event.stopPropagation()">
|
||
<div class="form-group">
|
||
<label>Bot ID</label>
|
||
<input type="text" id="ch-wecom-botid" placeholder="aib...">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Secret</label>
|
||
<input type="text" id="ch-wecom-secret" placeholder="密钥">
|
||
<p class="hint"><a href="https://work.weixin.qq.com" target="_blank">→ 从企业微信管理后台获取</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="model-card channel-card" style="grid-column: 1 / -1; border-color: #07c160;" onclick="startWeChatLogin(event)">
|
||
<h4>💬 微信 <span class="tag" style="background:rgba(7,193,96,0.2);color:#07c160;">个人微信</span></h4>
|
||
<p>扫码接入个人微信,用微信聊天控制 AI</p>
|
||
<div class="channel-form" id="form-wechat" onclick="event.stopPropagation()" style="max-height:none;overflow:visible;">
|
||
<div id="wechat-qr" style="text-align:center;padding:16px;">
|
||
<p style="color:#888;font-size:0.85em;">点击此卡片开始扫码</p>
|
||
</div>
|
||
<div id="wechat-status" style="text-align:center;color:#888;font-size:0.85em;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 28px; text-align: center;">
|
||
<button class="btn btn-primary" onclick="saveChannelsAndOpen()" style="font-size: 1.15em; padding: 16px 48px;">🚀 打开 Dashboard</button>
|
||
<p style="color: #888; font-size: 0.8em; margin-top: 10px;">渠道配置会自动写入,Gateway 需重启生效</p>
|
||
</div>
|
||
|
||
<div class="btn-row" style="justify-content: center; margin-top: 16px;">
|
||
<button class="btn btn-secondary" onclick="goStep(1)">重新配置模型</button>
|
||
<button class="btn btn-secondary" onclick="viewConfig()">查看配置文件</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Skills & info (always visible) -->
|
||
<div class="section active skills-section" style="display:block; margin-top: 8px;">
|
||
<h2>📦 目录说明</h2>
|
||
<p class="desc">U 盘内的文件结构</p>
|
||
<div class="skill-list">
|
||
<div class="skill-item">
|
||
<div class="info">
|
||
<h4>skills-cn/ — 中文技能</h4>
|
||
<p>放入 .md 技能文件,OpenClaw 自动加载</p>
|
||
</div>
|
||
<span class="status">📁</span>
|
||
</div>
|
||
<div class="skill-item">
|
||
<div class="info">
|
||
<h4>app/ — 运行时 + 核心</h4>
|
||
<p>Node.js 运行时、OpenClaw 核心代码</p>
|
||
</div>
|
||
<span class="status">⚙️</span>
|
||
</div>
|
||
<div class="skill-item">
|
||
<div class="info">
|
||
<h4>data/ — 配置和数据</h4>
|
||
<p>API Key、聊天历史等保存在此(仅本地)</p>
|
||
</div>
|
||
<span class="status">🔒</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
// --- State ---
|
||
let selectedProvider = null;
|
||
let selectedBase = '';
|
||
let selectedModel = '';
|
||
|
||
// --- Model cards ---
|
||
document.querySelectorAll('#step1 .model-card').forEach(card => {
|
||
card.addEventListener('click', (e) => {
|
||
if (e.target.closest('.buy-link')) return;
|
||
document.querySelectorAll('#step1 .model-card').forEach(c => c.classList.remove('selected'));
|
||
card.classList.add('selected');
|
||
selectedProvider = card.dataset.provider;
|
||
selectedBase = card.dataset.base;
|
||
selectedModel = card.dataset.model;
|
||
document.getElementById('nextStep1').disabled = false;
|
||
});
|
||
});
|
||
|
||
document.getElementById('nextStep1').addEventListener('click', () => goStep(2));
|
||
|
||
// --- Steps ---
|
||
function goStep(n) {
|
||
document.querySelectorAll('.section:not(.skills-section)').forEach(s => s.classList.remove('active'));
|
||
document.getElementById('step' + n).classList.add('active');
|
||
document.querySelectorAll('.steps .step').forEach((s, i) => {
|
||
s.classList.remove('active', 'done');
|
||
if (i + 1 < n) s.classList.add('done');
|
||
if (i + 1 === n) s.classList.add('active');
|
||
});
|
||
if (n === 2) setupStep2();
|
||
}
|
||
|
||
const PROVIDER_NAMES = {
|
||
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
|
||
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
|
||
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
|
||
siliconflow: '硅基流动', custom: '自定义'
|
||
};
|
||
|
||
function setupStep2() {
|
||
var isCustom = selectedProvider === 'custom';
|
||
document.getElementById('customFields').style.display = isCustom ? 'block' : 'none';
|
||
|
||
var name = PROVIDER_NAMES[selectedProvider] || selectedProvider;
|
||
document.getElementById('step2desc').textContent = '请填写 ' + name + ' 的 API Key';
|
||
|
||
var card = document.querySelector('#step1 .model-card[data-provider="' + selectedProvider + '"]');
|
||
var buyLink = card ? card.querySelector('.buy-link') : null;
|
||
var linksDiv = document.getElementById('providerLinks');
|
||
linksDiv.innerHTML = '';
|
||
if (buyLink) {
|
||
linksDiv.innerHTML = '<a href="' + buyLink.href + '" target="_blank">🔑 ' + name + ' — 获取 API Key</a>';
|
||
}
|
||
linksDiv.innerHTML += '<a href="https://platform.minimaxi.com/" target="_blank">🔑 MiniMax — 国内推荐,注册送额度</a>' +
|
||
'<a href="https://open.bigmodel.cn/" target="_blank">🔑 智谱 GLM — 有免费额度</a>' +
|
||
'<a href="https://cloud.siliconflow.cn/" target="_blank">🔑 硅基流动 — 多模型聚合,便宜</a>';
|
||
}
|
||
|
||
// --- Toggle password ---
|
||
function togglePw(id) {
|
||
var inp = document.getElementById(id);
|
||
var btn = inp.parentElement.querySelector('.toggle-pw');
|
||
if (inp.type === 'password') { inp.type = 'text'; btn.textContent = '隐藏'; }
|
||
else { inp.type = 'password'; btn.textContent = '显示'; }
|
||
}
|
||
|
||
// --- Build OpenClaw config ---
|
||
function buildConfig(provider, base, model, apiKey) {
|
||
var baseConfig = {
|
||
gateway: { auth: { mode: 'token', token: 'uclaw' } },
|
||
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
||
meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() }
|
||
};
|
||
|
||
// 智谱 GLM uses built-in zai provider
|
||
if (provider === 'zai') {
|
||
return Object.assign({}, baseConfig, {
|
||
env: { ZAI_API_KEY: apiKey },
|
||
agents: { defaults: { model: { primary: 'zai/' + model } } }
|
||
});
|
||
}
|
||
|
||
var providerName = provider || 'myProvider';
|
||
var providers = {};
|
||
providers[providerName] = {
|
||
baseUrl: base,
|
||
apiKey: apiKey,
|
||
api: 'openai-completions',
|
||
models: [{
|
||
id: model,
|
||
name: model,
|
||
reasoning: false,
|
||
input: ['text'],
|
||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||
contextWindow: 128000,
|
||
maxTokens: 8192
|
||
}]
|
||
};
|
||
return Object.assign({}, baseConfig, {
|
||
models: { mode: 'merge', providers: providers },
|
||
agents: { defaults: { model: { primary: providerName + '/' + model } } }
|
||
});
|
||
}
|
||
|
||
// --- Save model config ---
|
||
async function saveConfig() {
|
||
var apiKey = document.getElementById('apiKey').value.trim();
|
||
if (!apiKey) { showToast('请填写 API Key', true); return; }
|
||
|
||
var base = selectedBase;
|
||
var model = selectedModel;
|
||
|
||
if (selectedProvider === 'custom') {
|
||
base = document.getElementById('customBase').value.trim();
|
||
model = document.getElementById('customModel').value.trim();
|
||
if (!base || !model) { showToast('请填写 API 地址和模型名称', true); return; }
|
||
}
|
||
|
||
var config = buildConfig(selectedProvider, base, model, apiKey);
|
||
|
||
try {
|
||
var res = await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(config)
|
||
});
|
||
if (!res.ok) throw new Error('保存失败');
|
||
showToast('配置已保存!');
|
||
goStep(3);
|
||
} catch (e) {
|
||
showToast('保存失败: ' + e.message, true);
|
||
}
|
||
}
|
||
|
||
// --- Channel toggle ---
|
||
function toggleChannel(card, type) {
|
||
var form = document.getElementById('form-' + type);
|
||
var isOpen = form.classList.contains('open');
|
||
if (isOpen) {
|
||
form.classList.remove('open');
|
||
card.classList.remove('selected');
|
||
} else {
|
||
form.classList.add('open');
|
||
card.classList.add('selected');
|
||
var firstInput = form.querySelector('input');
|
||
if (firstInput) setTimeout(function() { firstInput.focus(); }, 100);
|
||
}
|
||
}
|
||
|
||
// --- WeChat login ---
|
||
var wechatSession = null;
|
||
var wechatPolling = false;
|
||
|
||
async function startWeChatLogin(e) {
|
||
var form = document.getElementById('form-wechat');
|
||
// If form is already open and showing QR, don't restart
|
||
if (form.classList.contains('open') && wechatPolling) {
|
||
return;
|
||
}
|
||
|
||
// Toggle form open
|
||
var card = form.closest('.channel-card');
|
||
if (!form.classList.contains('open')) {
|
||
form.classList.add('open');
|
||
card.classList.add('selected');
|
||
}
|
||
|
||
var qrDiv = document.getElementById('wechat-qr');
|
||
var statusDiv = document.getElementById('wechat-status');
|
||
qrDiv.innerHTML = '<p style="color:#888;">正在获取二维码...</p>';
|
||
statusDiv.textContent = '';
|
||
|
||
try {
|
||
var res = await fetch('/api/wechat/start', { method: 'POST' });
|
||
var data = await res.json();
|
||
if (data.error) throw new Error(data.error);
|
||
|
||
wechatSession = data.sessionKey;
|
||
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
|
||
statusDiv.textContent = '请用微信扫描上方二维码';
|
||
statusDiv.style.color = '#07c160';
|
||
|
||
// Start polling
|
||
wechatPolling = true;
|
||
pollWeChatStatus();
|
||
} catch (err) {
|
||
qrDiv.innerHTML = '<p style="color:#f44336;">获取二维码失败</p>';
|
||
statusDiv.textContent = err.message;
|
||
statusDiv.style.color = '#f44336';
|
||
}
|
||
}
|
||
|
||
async function pollWeChatStatus() {
|
||
if (!wechatSession || !wechatPolling) return;
|
||
|
||
try {
|
||
var res = await fetch('/api/wechat/status?session=' + encodeURIComponent(wechatSession));
|
||
var data = await res.json();
|
||
|
||
var qrDiv = document.getElementById('wechat-qr');
|
||
var statusDiv = document.getElementById('wechat-status');
|
||
|
||
if (data.status === 'confirmed') {
|
||
wechatPolling = false;
|
||
qrDiv.innerHTML = '<div style="font-size:3em;">✔</div>';
|
||
statusDiv.innerHTML = '<span style="color:#4caf50;font-weight:600;">微信连接成功!</span><br><span style="font-size:0.8em;">账号: ' +
|
||
(data.accountId || '') + (data.pluginInstalled ? '' : '<br>⚠️ 插件未安装,请重启 Gateway') + '<br>需重启 Gateway 生效</span>';
|
||
statusDiv.style.color = '#4caf50';
|
||
showToast('微信连接成功!');
|
||
return;
|
||
}
|
||
|
||
if (data.status === 'refreshed') {
|
||
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
|
||
statusDiv.textContent = '二维码已刷新,请重新扫码';
|
||
statusDiv.style.color = '#ff6b35';
|
||
} else if (data.status === 'scaned') {
|
||
statusDiv.textContent = '已扫码,请在手机上确认...';
|
||
statusDiv.style.color = '#07c160';
|
||
} else if (data.status === 'expired') {
|
||
wechatPolling = false;
|
||
qrDiv.innerHTML = '<p style="color:#888;">二维码已过期,<a href="#" onclick="startWeChatLogin(event);return false;" style="color:#ff6b35;">重新获取</a></p>';
|
||
statusDiv.textContent = data.message || '';
|
||
statusDiv.style.color = '#f44336';
|
||
return;
|
||
} else if (data.status === 'error') {
|
||
wechatPolling = false;
|
||
qrDiv.innerHTML = '<p style="color:#f44336;">连接失败</p>';
|
||
statusDiv.textContent = data.message || 'Unknown error';
|
||
statusDiv.style.color = '#f44336';
|
||
return;
|
||
}
|
||
// wait -> continue polling
|
||
setTimeout(pollWeChatStatus, 1500);
|
||
} catch (err) {
|
||
setTimeout(pollWeChatStatus, 3000);
|
||
}
|
||
}
|
||
|
||
// --- Save channels and open dashboard ---
|
||
async function saveChannelsAndOpen() {
|
||
var channels = {};
|
||
|
||
var tgToken = document.getElementById('ch-telegram-token').value.trim();
|
||
if (tgToken) channels.telegram = { enabled: true, token: tgToken, dmPolicy: 'pairing' };
|
||
|
||
var qqId = document.getElementById('ch-qqbot-appid').value.trim();
|
||
var qqSecret = document.getElementById('ch-qqbot-secret').value.trim();
|
||
if (qqId && qqSecret) channels.qqbot = { enabled: true, appId: qqId, clientSecret: qqSecret };
|
||
|
||
var fsId = document.getElementById('ch-feishu-appid').value.trim();
|
||
var fsSecret = document.getElementById('ch-feishu-secret').value.trim();
|
||
if (fsId && fsSecret) channels.feishu = { enabled: true, appId: fsId, appSecret: fsSecret };
|
||
|
||
var wcBotId = document.getElementById('ch-wecom-botid').value.trim();
|
||
var wcSecret = document.getElementById('ch-wecom-secret').value.trim();
|
||
if (wcBotId && wcSecret) channels.wecom = { enabled: true, botId: wcBotId, secret: wcSecret };
|
||
|
||
if (Object.keys(channels).length > 0) {
|
||
try {
|
||
var res = await fetch('/api/config');
|
||
var existing = res.ok ? await res.json() : {};
|
||
// 清除旧版废弃键,防止 OpenClaw 报 "agent.* was moved" 错误
|
||
delete existing.agent;
|
||
existing.channels = Object.assign(existing.channels || {}, channels);
|
||
await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(existing)
|
||
});
|
||
showToast('渠道配置已保存');
|
||
} catch(e) {}
|
||
}
|
||
|
||
window.open(gatewayUrl(), '_blank');
|
||
}
|
||
|
||
// --- View config ---
|
||
async function viewConfig() {
|
||
try {
|
||
var res = await fetch('/api/config');
|
||
var config = await res.json();
|
||
var text = JSON.stringify(config, null, 2);
|
||
var w = window.open('', '_blank');
|
||
w.document.write('<pre style="background:#111;color:#eee;padding:20px;font-size:14px;">' +
|
||
text.replace(/</g, '<') + '</pre>');
|
||
} catch (e) {
|
||
showToast('读取配置失败: ' + e.message, true);
|
||
}
|
||
}
|
||
|
||
// --- Toast ---
|
||
function showToast(msg, isError) {
|
||
var t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.className = 'toast show' + (isError ? ' error' : '');
|
||
setTimeout(function() { t.className = 'toast'; }, 3000);
|
||
}
|
||
|
||
// --- Find active gateway port (18789-18799) ---
|
||
var _gatewayPort = null;
|
||
async function findGatewayPort() {
|
||
for (var p = 18789; p <= 18799; p++) {
|
||
try {
|
||
await fetch('http://127.0.0.1:' + p + '/', { mode: 'no-cors' });
|
||
_gatewayPort = p;
|
||
return p;
|
||
} catch(e) {}
|
||
}
|
||
_gatewayPort = null;
|
||
return null;
|
||
}
|
||
|
||
function gatewayUrl() {
|
||
var p = _gatewayPort || 18789;
|
||
return 'http://127.0.0.1:' + p + '/#token=uclaw';
|
||
}
|
||
|
||
// --- Check gateway status ---
|
||
async function checkGateway() {
|
||
var dot = document.getElementById('statusDot');
|
||
var text = document.getElementById('statusText');
|
||
var port = await findGatewayPort();
|
||
if (port) {
|
||
dot.className = 'dot on';
|
||
text.innerHTML = 'Gateway 运行中 (端口 ' + port + ') · <a href="' + gatewayUrl() + '" target="_blank">打开 Dashboard →</a>';
|
||
document.getElementById('gatewayUrl').textContent = 'http://127.0.0.1:' + port;
|
||
} else {
|
||
dot.className = 'dot off';
|
||
text.innerHTML = 'Gateway 未启动 · 请先运行 Windows-Start.bat 或 Mac-Start.command';
|
||
}
|
||
}
|
||
|
||
// --- Load existing config ---
|
||
async function loadConfig() {
|
||
try {
|
||
var res = await fetch('/api/config');
|
||
var cfg = await res.json();
|
||
|
||
if (cfg.env && cfg.env.ZAI_API_KEY) {
|
||
var card = document.querySelector('#step1 .model-card[data-provider="zai"]');
|
||
if (card) card.click();
|
||
document.getElementById('apiKey').value = cfg.env.ZAI_API_KEY;
|
||
return;
|
||
}
|
||
|
||
if (cfg.models && cfg.models.providers) {
|
||
var providerName = Object.keys(cfg.models.providers)[0];
|
||
if (providerName) {
|
||
var p = cfg.models.providers[providerName];
|
||
var card = document.querySelector('#step1 .model-card[data-provider="' + providerName + '"]');
|
||
if (card) card.click();
|
||
if (p.apiKey) document.getElementById('apiKey').value = p.apiKey;
|
||
}
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
loadConfig();
|
||
checkGateway();
|
||
setInterval(checkGateway, 10000);
|
||
</script>
|
||
</body>
|
||
</html>
|